3
votes

While trying to get some insights on compilers behaviors (gcc and clang) related to this question, I just did not understand why there was a difference in the 3rd case (presented below) between gcc and clang. The question is not about the correctness of such a conversion API (especially the reference case).

Could you please help me understand what is the expected behavior (from a c++ standard point of view) in this scenario?

EDIT: As stated in the comments, this behavior is observable in clang only from -std=c++17. Before that, reference conversion is used as in gcc.

EDIT2: Note that the right behavior "seems" to be gcc as the implicit this argument is not const thus the non-const overload is preferred...

Here is the sample code:

struct SInternal {
    SInternal() = default;
    SInternal(const SInternal&) {
        std::cout << "copy ctor" << std::endl;
    }
    int uuid{0};
};

struct S {
 SInternal s;

 S() = default;

 operator SInternal() const {
     std::cout << "copy conversion" << std::endl;
     return s;
 }

 operator SInternal& () {
     std::cout << "ref conversion" << std::endl;
     return s;
 }
};

int main() {
    S s;
    const S s2;
    // 1-
    //SInternal si = s; // no ambiguity, ref conversion
    //SInternal si = s2; // no ambiguity, copy conversion
    // 2-
    // SInternal& si = s; // no ambiguity, ref conversion
    // SInternal& si = s2; // no viable conversion operator SInternal& not const
    // Case 3- WHAT IS THE CORRECT EXPECTED BEHAVIOR HERE?
    SInternal si(s); // no ambiguity but clang uses copy conversion
                     // while gcc uses ref conversion
    //SInternal si(s2); // no ambiguity, copy conversion
    // 4-
    //SInternal si = std::move(s); // no ambiguity ref conversion

    std::cout << "test " << si.uuid << std::endl;
}

DEMO HERE.

Thanks for your help.

1
Spamming the version tags only makes pinpointing an answer harder. Please confine yourself only to the standard you are actually interested in.StoryTeller - Unslander Monica
@StoryTeller-UnslanderMonica Thanks for the tips! I was just trying to reach a wider audience but you are right as I am interested in the latest version onlynop666
it's interesting that clang does the same as gcc if -std=c++14. I think RVO in c++17 makes operator T() is better than operator T&() + T(T const&). and in c++14, operator T&() + T(T const&) is better than operator T() + T(T const&), because T& is better matched with T const& than T. refer to: over.match.bestRedFog
@RedFog Both compilers are aware of guaranteed copy elision. Don't you think it is the other way around ? Clang selects the by-value version and, then, as it returns by value, applies some optimization. Overload resolution first.nop666
@nop666 in my opinion, RVO is considered at the implicit conversion sequence in clang, but in gcc it isn't. but some lawyers that I asked said, operator T() and operator T& are both exact match , so choosing operator T&() is better because s is non-const. so waiting for more answer as well.RedFog

1 Answers

1
votes

Here is an attempt to answer my own question from my research so far and the kind help in the comments.

Any remarks in the comments are very welcome to improve the answer.

The answer is closely related to this answer and that one thus the question itself might be a duplicate.

gcc case

  1. This is a case of direct initialization (SInternal si(s);). This case falls under dcl.init:

Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one is chosen through overload resolution. The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.

Result: SInternal() and SInternal(const SInternal&) are considered and SInternal(const SInternal&) is selected

  1. A reference must be bound to SInternal (SInternal is the type of the reference being initialized, S is the type of the initializer expression). This case falls under over.match.ref:

    ... The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” (when initializing an lvalue reference or an rvalue reference to function) or “ cv2 T2” or “rvalue reference to cv2 T2” (when initializing an rvalue reference or an lvalue reference to function), where “cv1 T” is reference-compatible ([dcl.init.ref]) with “cv2 T2”, are candidate functions ...

Result: candidate functions are operator SInternal() const and operator SInternal& ()

  1. Overload resolution. This case falls under [over.match.ref](This case falls under over.ics.rank):

Result: operator SInternal& () selected because implicit this parameter is non-const.

  1. Final result pseudo-code: SInternal(operator SInternal& ())

clang case

It seems that the behavior is related to CWG 2327 as explained in other posts.

If this is the compiler implementation of such a behavior, conversion function is considered for direct-initialization and operator SInternal () const is selected.

The last point is the implementation of the conversion operator. If SInternal copy ctor is made trivial, no call to the copy constructor is made. If an empty copy constructor is defined, then it is called (DEMO).

This is due to the fact that SInternal becomes TriviallyCopyable and thus the compiler takes profit of that to use the register for the copy. If you fill up SInternal with more data members (e.g. char arr[32];), it will end up using memcpy.