2
votes

When passing variable arguments to a statement, the type information does not get passed.

Imagine this example using a string object (I'm using MFC/ATL's CString)

CString name = "kevin";
printf("Hi %s", name);

How does the printf statement (or the CString) know to grab/return the LPCSTR operator and thus get a pointer to the string, and not a pointer to the object?

I'm trying to write my own string class (it needs to replace CString in a huge project), but when I do the above printf, it ends up garbled because it didn't get the correct value for name.

3
Well, probably, because CString has operator LPCTSTR overloaded. - Algirdas Preidžius
I don't know whether MSVC has this yet (I think it might have appeared recently or might be coming in VS2015 Update 1 or something), but GCC gives a warning for mismatching types to printf and Clang actually gives an error (at least in my example case). Actually, I found a case with CString that VS2015 complains about. - chris
@AlgirdasPreidžius: The compiler will only ever invoke an implicit type conversion, if it knows source and target type. With variadic functions, though, there is no target type specified, so the compiler cannot perform any conversions. - IInspectable
Now for the real question: Why would you ever attempt to replace CString in a huge project? It's still there, after decades, simply because it works. It implements conversion between MBCS and Unicode, plays well with C-style arguments, and sports COM support. - IInspectable
My class derives from CString, and also has LPCTSTR overloaded, but it still doesn't work :( Casting definitely works, but I don't want to go back and cast everywhere. CString is awesome, but I need to use it in a heavily multi-threaded environment so can't have it sharing -- need to force every instance to be unshared. - DougN

3 Answers

3
votes

The real answer has nothing to do with an implicit call to operator LPCTSTR (), and I think it's kind of clever how it actually works.

To implement a string class, you need the string's data buffer, of course, as well as some other data fields, like the length of the string, maybe how much memory is allocated, maybe a reference count, etc.

But how would you make variadic functions see a C-style string when passing a CString argument? You could put the data buffer field (a pointer to char*) as the first member, but then all the other fields (like string length) also get unexpectedly pushed onto the stack as if they were extra function arguments.

So the next thing you try is to package all of the fields into a single memory structure which is allocated along with the string'd character data buffer. And if this data buffer is located at the start of the aggregate structure, a variadic function expecting a C-string will see the beginning of this structure. If a terminal null is placed at the end of the data buffer, before the other data fields, then printf will stop reading string data before it hits the data you don't want it to see and everything works out fine!

But then you realize you have a new problem -- the length of the character data buffer is variable, and the "length" field is now located in memory after the character data, so you would need to know "length" in order to know where the "length" field can be found.

So can you move "length" and all the other extra fields to the beginnning of the structure, in memory before the character data? Of course not, printf would now see some funny characters (or even a null) added at the beginning of the real string.

But this is what CString does. However, its single member variable is a pointer, not to the start of the aggregate struct, but to the memory location inside it where the character data begins.

And because the data fields (without the character buffer) have a fixed size, the string class knows how to access them in memory, given its pointer to char (these fields are located at a negative offset from the pointer to char). And of course, when it frees its aggregate structure, the class will also need to calculate where its original memory allcation begins, not just use its pointer to char.

1
votes

The reason it ends up garbled is because C-style vararg passing does no type conversion whatsoever. You're passing in an object, which might have many fields and an arbitrary layout, to a function that does the same thing in spirit as casting it to a void* -- i.e. it erases the type information. If you want your code to work you need to cast it to the type you expect the function to see. In this case, you need to write:

printf("Hi %s", (LPCSTR)name);

This way the value that actually gets passed to the printf function is a pointer to the underlying string, which is what the %s format specifier expects.

1
votes

How does the printf statement (or the CString) know to grab/return the LPCSTR operator and thus get a pointer to the string, and not a pointer to the object?

It doesn't. printf isn't typesafe. It will blindly pretend that name is a C string and not a C++ class object. This is undefined behaviour, allowing the program to do whatever it wants, including work as expected.

To get this to work properly, you either have to cast the object explicitly or use something that is typesafe, unlike printf.

If the call seems to work properly, there's a chance that CString's first or only data member is a C string of the appropriate type whose address is the same as the object's (similar to this example). However, this behaviour should never be relied upon.