I'm writing a interface to a 3rd party library. It manipulates objects through a C interface which essentially is a void*. Here is the code simplified:
struct LibIntf
{
LibIntf() : opaquePtr{nullptr} {}
operator void *() /* const */ { return opaquePtr; }
operator void **() { return &opaquePtr; }
void *opaquePtr;
};
int UseMe(void *ptr)
{
if (ptr == (void *)0x100)
return 1;
return 0;
}
void CreateMe(void **ptr)
{
*ptr = (void *)0x100;
}
int main()
{
LibIntf lib;
CreateMe(lib);
return UseMe(lib);
}
Everything works great until I add the const on the operator void *() line. The code then defaults silently to using the operator void **() breaking the code.
My question is why?
I'm returning a pointer through a function that doesn't modify the object. Should be able to mark it const. If that changes it to a const pointer, the compiler should error because operator void **() shouldn't be a good match for function CallMe() that just want a void *.
This is what the standard says should happen, but this is far from obvious. For quick readers, jump to the "How to fix it?" at the end.
const mattersOnce you add the const qualifier, when you call UseMe with an instance of LibIntf, the compiler have the two following possibilities:
LibIntf →1 LibIntf →2 void** →3 void* (through operator void**())LibIntf →3 const LibIntf →2 void* →1 void* (through operator void* const())1) No conversion needed.
2) User-defined conversion operator.
3) Legal conversions.
Those two conversion paths are legal, so which one to choose?
The standard defining C++ answers:
[over.match.best]/1Define
ICSi(F)as follows:
- [...]
- let
ICSi(F)denote the implicit conversion sequence that converts theith argument in the list to the type of theith parameter of viable function F.[over.best.ics]defines the implicit conversion sequences and[over.ics.rank]defines what it means for one implicit conversion sequence to be a better conversion sequence or worse conversion sequence than another.Given these definitions, a viable function
F1is defined to be a better function than another viable functionF2if for all argumentsi,ICSi(F1)is not a worse conversion sequence thanICSi(F2), and then
for some argument
j,ICSj(F1)is a better conversion sequence thanICSj(F2), or, if not that,the context is an initialization by user-defined conversion (see
[dcl.init],[over.match.conv], and[over.match.ref]) and the standard conversion sequence from the return type ofF1to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type ofF2to the destination type.
(I had to read it a couple times before getting it.)
This all means in your specific case than option #1 is better than option #2 because for user-defined conversion operators, the conversion of the return type (void** to void* in option #1) is considered after the conversion of the parameter type (LibIntf to const LibIntf in option #2).
In chain, this means in option #1 there is nothing to convert (latter in the conversion chain, there will but this is not yet considered) but in option #2 a conversion from non-const to const is needed. Option #1 is, thus, dubbed better.
Simply remove the need to consider the non-const to const conversion by casting the variable to const (explicitly (casts are always explicit (or are called conversions))):
struct LibIntf
{
LibIntf() : opaquePtr{nullptr} {}
operator void *() const { return opaquePtr; }
operator void **() { return &opaquePtr; }
void *opaquePtr;
};
int UseMe(void *ptr)
{
if (ptr == (void *)0x100)
return 1;
return 0;
}
void CreateMe(void **ptr)
{
*ptr = (void *)0x100;
}
int main()
{
LibIntf lib;
CreateMe(lib);
// unfortunately, you cannot const_cast an instance, only refs & ptrs
return UseMe(static_cast<const LibIntf>(lib));
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With