Some C++ Gotchas

Some C++ Gotchas

Rate This
  • Comments 26

Hi - Jonathan Caves again.  Over the last couple of weeks I’ve seen some reports from users that the C++ compiler does not act the way they think it should.  As it turns these reports weren’t real bugs, but the issues brought up are interesting enough to share with a wider audience.

 

The first was from a customer who reported that the compiler was calling the wrong function.  The problem code can be reduced to the following:

 

class string {

public:

string(const char*);
};

 

void f(string, string, bool = false);

void f(string, bool = false);

 

void g()

{

f(“Hello”, “Goodbye”);
}

 

The user’s observation was that the compiler should call the first function:

 

void f(string, string, bool = false);

 

but in reality it was calling the second function:

 

void f(string, bool = false);

 

and they thought that this was a bug.  At first glance it does appear that the user is correct - but looks can be deceiving.  Just because the string class has a converting-constructor from a string-literal doesn’t mean that the compiler has to use it.  For the first argument to the function call, the conversion is straight forward - both of the candidate functions expect a string and so the compiler will use the provided converting constructor to convert the string-literal to an instance of the string class. The second argument is not so straight forward.  For the first function the compiler can again use the converting-constructor, but for the second function it can use the standard pointer-to-bool conversion to convert the string-literal (which the compiler will consider as type “const char*”) to bool.  As this is a standard conversion, the C++ Standard considers this conversion to be cheaper than calling the converting-constructor (which is a user-defined conversion) and hence the second function is a better match than the first function and the compiler, correctly, calls that function.

 

Note: the real issue here is with the use of default-arguments.  Without default-arguments the user would not be left to the mercy of the C++ conversion rules.  If they wanted to call the three parameter version then they would need to provide three arguments; if they want to call the two parameter version then they need to provide two arguments. At first glance, default-arguments seem to be a great C++ language feature but I have seen them cause users no end of problems. I’ve even seen users doing stuff like the following:

 

SomeFunction(arg1, arg2 /*, arg3 = false, arg4 = true */);

 

They do this just so it is clear to readers of the code that default arguments are being used.  If you are going to go this far then just get rid of the default arguments (and the comments).  Believe me that it will make your life much easier and your code more maintainable!

 

The second issue was around code that compiled but when the user applied what they thought was a minor edit the code no longer compiled. The code could be reduced to the following:

 

namespace std {

   template<typename T>

   class list {

   public:

      size_t size() const;

   };
}

 

class X : std::list<int> {

public:

   size_t mf1() const { return list::size();      }
   size_t mf2() const { return std::list::size(); }
};

 

The problem was that while mf1 compiles fine, mf2 generates the following error message:

 

a.cpp(12) : error C2955: 'std::list' : use of class template requires template argument list

        a.cpp(3) : see declaration of 'std::list'

 

The user’s question was why? Surely if the first function compiles then the second function should also compile because all the user has done is to make it clearer to the compiler what was going on. But the problem was that they have been too specific. It all comes down to something in C++ called the “injected-class-name”. 

 

In C++, each class has a member that is added – injected – by the compiler and this member has the same name as the class. (Note: don’t confuse this with a constructor which only looks as if it has the same name as the class. In reality constructors have no-name or at least a name that cannot be written in C++ code.) This member is needed in order that there can be rules for defining a constructor outside of a class.  Without this injected-member the C++ Standard would have to revert to hand-waving - something writers of Standards really hate.  One further twist is that in the case that the class is a specialization of a class template, there are two versions of the injected-class-name: one is the name of the class without the template-arguments, list in the example above, and the other is the name of the class with the template-arguments, list<int> in the example above.

 

So in the first example when the compiler sees the identifier, list, it does normal name-lookup.  It first looks up ‘list’ in the current class scope and finds nothing.  It then looks up ‘list’ in the scope of the base class where it finds the injected-class-name and, eventually, works out that by ‘list’ the user means ‘std::list<int>’ which is a base-class of the current class. So the compiler treats the code as-if the user had written:

 

     size_t mf1() const { return list<int>::size(); }

In the second example the compiler first sees ‘std’ which it looks-up and finds the namespace std.  It then looks up ‘list’ within the namespace ‘std’ and finds the class-template - but this is not an injected-class-name – and there is no version in which the template-arguments are implied. So in this case the user needs to explicitly provide the template-arguments, hence the error message.  

 

I think the lesson here is don’t try to help the compiler – let it resolve the code by itself.  If you have got it wrong it will tell you.  If it compiles and you want to double check the result, then you should debug the code (you already do step through all the code you write in the debugger - don’t you?). The worse offence in this category are people who add casts thinking that they will force the compiler to compile the code they way they want it to be complied. At best the cast is unnecessary (and hence makes the code more difficult to maintain) and worst it will lead to hard-to-detect runtime errors.

 

Thanks,

Jonathan

  • Sorry to abuse this thread but I have no other way at the moment.  MSDN forums repeatedly give HTTP 500's when I try to log in and/or post.  This being a C++ gotcha of a sort, this is the least off-topic place where I can post at the moment.

    C++ code:

    if (multibyteToUnicode(m_SoftwareCodePage, pPathname, m_pUnicodePathname,

    (unicodePathnameLen * sizeof(unsigned short))

    != unicodePathnameLen)) {

    Datatypes:

    unsigned int m_SoftwareCodePage

    char *pPathname

    unsigned short *m_pUnicodePathname

    size_t unicodePathnameLen

    Function type:

    size_t multibyteToUnicode(unsigned int codePage, const char *pMultibyteString,

    unsigned short *pUnicodeBuffer, size_t bufferCount);

    Values:

    m_SoftwareCodePage == 932

    pPathname == a valid pointer, pointing to a valid string

    m_pUnicodePathname == a valid pointer, pointing to a malloc'ed block of 32 bytes

    unicodePathnameLen == 16

    Value computed by Norman Diamond:

    (unicodePathnameLen * sizeof(unsigned short)) == 32

    Value computed by Visual Studio 2008:

    (unicodePathnameLen * sizeof(unsigned short)) == 1

    Code generated by Visual Studio 2008:

    0041C08B  mov         eax,dword ptr [ebp-30h]

    0041C08E  shl         eax,1

    0041C090  xor         ecx,ecx

    0041C092  cmp         eax,dword ptr [ebp-30h]

    0041C095  setne       cl

    0041C098  mov         esi,esp

    0041C09A  push        ecx

    Maybe size_t * size_t  -->  bool  -->  size_t ?

  • Uh yeah, it went to bool because my parentheses were in the wrong place.  Sorry to disturb some number of readers and blog owner.

  • Regarding the compiler "calling the wrong function", I recently spent half a day chasing around in circles on an unrelated problem that will match the same search terms.

    For functions with identical object code and at sufficiently high optimization levels, the linker will combine the bodies.  When it does, the debugger, performance profiler, etc will pick one of the names more-or-less at random.  It looks very much like the wrong function got called.

    Caveat Debugger.

  • The MSDN forums are still giving ASP error 500 every time I try to log in and/or post.  I see other people have posted but I can't guess how they do it.  I'm using IE6 in Windows XP SP3, but the errors aren't in IE, they're server error 500's in Microsoft.

    Therefore I resort to posting here again.

    This time the problem isn't user error.  Intellisense is still the same in VC++2008 RTM as it was in VC++2005 betas.

    I close Visual Studio 2008, delete the .ncb file, double-click the .sln file again, wait for Visual Studio 2008 to rebuild broken Intellisense information, and it is rebuilt broken.

    If I hover the mouse cursor over an identifier, a tooltip might or might not display.  Today the failure occurs most often if the identifier is a member of a derived class.

    If I type code like

    this->

    there is an error that the left hand operator of -> is invalid.  It compiles correctly, but the bug isn't in the compiler, the bug is in Intellisense.

    If I type a member function name correctly and then a left parenthesis, no tooltip appears.

    In the pair of combo boxes above the code window, the left-hand combo box always says global scope, even if the text cursor is in the middle of a class's code.  The class name cannot be selected.  The right-hand combo box is empty and nothing can be selected.  Today this problem is happening only with derived classes but base classes are working.

    Anyway, Intellisense gets rebroken every time it gets rebuilt.  Please, is there any way to get Intellisense working again?

  • To David Roe

    I stumbled on this blog by accident (was searching something else). But I USED stress USED to be a C++ programmer. When I see blog entries like these I think, "gee I am glad I switched programming languages."

    When I need to switch to low level, believe it or not I use C! I find C a whole lot more comforting and straight forward... And with this functional drive coming implementing functional constructs in C is DEAD SIMPLE...

  • In my ~10 years of using Visual Studio, I've only encountered one previously unknown compiler bug.  It was a ridiculously obscure bug involving rare compilation conditions.

    In C and C++, pointers are considered signed for the purposes of casting.  This means that if you cast a pointer to an integer type of size larger than a pointer, sign extension is used.  This can occur with 32-bit compilation, and indeed works in this context:

    int wmain()

    {

     void* ptr = (void*) (intptr_t) 0x80000000;

     wprintf(L"%016llX", (long long) ptr);

    }

    This will print FFFFFFFF80000000 due to the sign extension, and is the expected answer.

    However, if the value is a compile-time constant referring to the address of a global object or function, the compiler can zero-extend.  This happens if the global object's address is larger than 7FFFFFFF on a 32-bit target:

    int var;

    long long address = (long long) &var;

    If the base address of the DLL has its high bit set, the compiler will emit a zero-extended address of var.  This can only occur when compiling Windows NT kernel drivers.

    The compiler or linker should actually error or warn instead of allowing this.  It is not possible in the 32-bit Portable Executable format to emit this type of relocation (sign-extend relocated address).  However, this case is so obscure that it was deemed unworthy of being fixed.

  • "In C and C++, pointers are considered signed for the purposes of casting."

    In the old days, before the "unsigned" keyword was invented, C hackers knew how to get one.  On the PDP-11, "int" was signed 16 bits, and "char*" was unsigned 16 bits.

    Now fast-forward to some popular implementations on modern hardware...

    "(intptr_t) 0x80000000"

    The result of the cast to intptr_t is signed because intptr_t is a signed type.  If intptr_t is 64 bits then I think the semantics are value preserving, signed value 0x0000000080000000.  If intptr_t is 32 bits then you get a signed result after the overflow.

    "(long long) &var"

    If the pointer is 32 bits then I think the semantics are value preserving.  I think your example demonstrated that the pointer representation is treated as an unsigned representation before being converted.

    I see why you want a warning when a pointer type is cast to a signed type, but I wonder if it would be more suitable in Prefast tests than built into the DDK's compiler.  Do you also want a warning when an unsigned int constant is cast to a signed type, to catch your first example too?  Maybe you should suggest these to the Prefast team?

  • I recommend that you do not download this beta. It totally damaged my SQL server and Framework up to the point that I cannot unistall and reinstall. It causes system problems. Its a real pain in my ass because I've tried everything to fix it and I've only attained partial progresss. I won't download a beta like this. I've heard rumors that Vista and new programs have problems but I didn't think I'd ever really have a problem because I know how to fix most problems but this is annoying. My system did not crash but everytime I start up it tells me that SQL is incomplete or corrupt. I like no problems but I guess I'm going to have to live with this one unless I reinstall all my programs which I don't want to do. frustration.

  • Betas are betas.  Now if betas were betas for a reason, so that bugs found in betas would not be present in releases, then betas would be really valuable.  But betas would still be betas.  You should know to download and install betas on machines that are dedicated to experimenting.

  • STL enjoys speaking in the third person and also enjoys bringing you this exclusive news: Visual Studio

  • From elsewhere in the collective.

Page 2 of 2 (26 items) 12