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.
Understanding why the const
matters
Once 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:
Define ICSi(F)
as follows:
- [...]
- let
ICSi(F)
denote the implicit conversion sequence that converts the i
th argument in the list to the type of the i
th 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 F1
is defined to be a
better function than another viable function F2
if for all arguments
i
, ICSi(F1)
is not a worse conversion sequence than ICSi(F2)
, and then
for some argument j
, ICSj(F1)
is a better conversion sequence than ICSj(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 of F1
to 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 of F2
to 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.
How to fix it?
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));
}