3

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

YSC
  • 38,212
  • 9
  • 96
  • 149
Jack Sanga
  • 33
  • 3
  • 2
    Try creating const and non-const versions of each operator, e.g., `operator const void*() const` and `operator void*()` (and the same for the `void**` variation). Also, shouldn't `UseMe` accept a `const void*`? – Phil Brubaker Nov 21 '17 at 14:39
  • I think the compiler is happy with the void** conversion because you can convert any pointer to void *, if you do not want that, is there a way to use a different type? Otherwise I think grek40's link explains everything. – PaulR Nov 21 '17 at 14:51
  • @Phil, UseMe is defined by the 3rd party. I can't change the signature. – Jack Sanga Nov 21 '17 at 16:03
  • @JackSanga Ah OK my misunderstanding. Skipped over the details in a rush to try to make a helpful comment. :-) – Phil Brubaker Nov 21 '17 at 16:11
  • @JackSanga still, the comment from Phil is close to a solution approach. Define both overloads of the conversion... `operator void const*() const` __and__ `operator void*()` and maybe even add a const overload for `void const* const*` – grek40 Nov 21 '17 at 16:43
  • @grek40 no reason to define the operator const if it will never be called. None of the 3rd party calls expect a const. Thanks to everybody who contributed. – Jack Sanga Nov 21 '17 at 17:54
  • @JackSanga this is not about the 3rd party... the difference is, whether the instance of your struct is const. If it is, it will only apply const conversion operators, otherwise it will prefer the not-const ones over the const ones even when the returned converted type needs another conversion. However, if your question is no longer *why* but *how to deal with it*, then the duplicate is not a perfect match anymore. – grek40 Nov 22 '17 at 00:05

1 Answers1

6

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));
}
YSC
  • 38,212
  • 9
  • 96
  • 149
  • See the following example for the difference between const/non-const overloads: https://ideone.com/4oUVmK. IMO `LibIntf2` there would be the correct one, no need to const-cast the instances on every usage. – grek40 Nov 22 '17 at 14:38
  • @grek40 Ho yeah that's better (I thought you meant to define const and non-const operator for both void* and void**). It's better, but it is still weird. – YSC Nov 22 '17 at 14:47
  • @YSC basic guideline to account for the strange overload resolution: always define the non-const conversion, optionally define the const conversion as additional operator instead of a replacement. – grek40 Nov 22 '17 at 14:59