Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change member function to const silently breaks code [duplicate]

Tags:

c++

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 *.

like image 229
Jack Sanga Avatar asked Feb 27 '26 08:02

Jack Sanga


1 Answers

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:

  1. LibIntf1 LibIntf2 void**3 void* (through operator void**())
  2. LibIntf3 const LibIntf2 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]/1

Define ICSi(F) as follows:

  • [...]
  • let ICSi(F) denote the implicit conversion sequence that converts the ith argument in the list to the type of the ith 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));
}
like image 129
YSC Avatar answered Feb 28 '26 23:02

YSC



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!