2

Consider the following code:

class Y {};
class X
{
public:
    X() { }
    X(const Y&) { }
    explicit X(const X&) { }
};

X f()
{
    return X();
}

int main()
{
    Y y;
    X x = y;
    f();
}

The code gives an error (cannot convert from 'X' to 'X') at the line return X();. In my understanding, this is because X(X const&) is declared as explicit and X(X const&) is "called" implicitly to copy-initialize the returned object from the object created by X(). Since there is copy elision, X() will be used to create the returned object directly when X(const X&) is not explicit. However, the explicit keyword stops the code even if X(X const&) won't really be called, so my guess here is that the explicit keyword does not care whether there is copy elision or not.

In my understanding, a copy initialization in the form like X x = y; will first try to convert y to type X, and then copy that object of type X converted from y into x (so X x = y; does something like X x{ X{y} }), and since there is copy elision, the object of type X converted from y will directly be constructed as x.

Then, I comment out the definition and the call of the function f(). I am expecting the same error happened at return X(); happens to X x = y, because if X x = y does something like X x{ X{y} }, it would implicitly call explicit X(const X&) if there is no copy elision, and from my guess above the explicit keyword should not care whether there is copy elision or not.

But there is no compiler error this time. So, I'm guessing that X x = y would not call X(const X&) even if there is no copy elision. I'm guessing that X x = y is just an implicit call of X(const Y&).


May I ask if my guesses are correct? If not, could someone please tell me where I go wrong, and why explicit X(const X&) is not affecting X x = y; while it stops return X();?

digito_evo
  • 3,216
  • 2
  • 14
  • 42
CPPL
  • 726
  • 1
  • 10
  • 1
    The major compilers (clang, gcc, msvc) all seem compile the code provided you compile with the appropriate version of the standard (>=17): https://godbolt.org/z/f3neav6Ps – fabian Jun 05 '22 at 09:50

1 Answers1

4

Your code compiles in C++17 in newer, since mandatory copy elision doesn't check for accessible copy/move constructors.

If you remove the function definition, the code is valid regardless of C++ version, because in X x = y;, x is direct-initialized from the temporary X (i.e. the copy constructor is called "explicitly", so explicit is fine). In C++17 and newer, there's no temporary X here in the first place.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • @user17732522 Can I say, in C++17 and after, `X x = y;` generates the same code as directly (though implicitly) calling `X(const Y&)` to construct `x`? – CPPL Jun 05 '22 at 13:52
  • 1
    @CPPL In this case yes, but in general no, because copy-initialization doesn't consider `explicit` constructors and there are more edge cases where they differ. If by "generate the same code" you mean the actual compiled binary code, it will almost certainly be the same before and after C++17 in this case, since even before C++17 the compiler was allowed to elide the intermediate temporary and compilers commonly did do that. It just had to check that the copy constructor is usable (accessible and non-deleted). – user17732522 Jun 05 '22 at 14:04
  • Also, could you please elaborate more on why initializing `x` from the temporary `X` calls the copy constructor explicitly? (before C++17) – CPPL Jun 05 '22 at 14:06
  • 1
    @HolyBlackCat That's the open [CWG issue 2327](https://open-std.org/JTC1/SC22/WG21/docs/cwg_active.html#2327). Currently no elision is specified. But I removed that part anyway from the comment, since it is an unusual edge case with specification issues, so probably not helpful to mention. – user17732522 Jun 05 '22 at 14:07
  • @CPPL Because the language specification says so in https://timsong-cpp.github.io/cppwp/n4140/dcl.init#17.6.2. (It is technically not really the copy constructor per-se, but direct-initialization of a `X` from a `X` will normally result in the copy or move constructor after overload resolution on the constructor.) And direct-initialization is specified to also consider `explicit` constructors. – user17732522 Jun 05 '22 at 14:10
  • *"why initializing x from the temporary X calls the copy constructor explicitly"* It makes sense to do so. This hides the fact that a temporary `X` is involved at all (which I consider a quirk of the language). Note that the other (converting) constructor is checked for `explicit` here. – HolyBlackCat Jun 05 '22 at 14:15