5

Consider the following code:

struct S
{
    S(int, double) {}
    explicit S(const S&) {}
    explicit S(S&&) {}
};

void i_take_an_S(S s) {}

S i_return_an_S() { return S{ 4, 2.0 }; }

int main()
{
    i_take_an_S(i_return_an_S());
}

With the '-std=c++17' flag, both g++ and clang++ compile this code just fine. MSVC (with the /std:c++17 flag), however, reports

"error C2664: 'void i_take_an_S(S)': cannot convert argument 1 from 'S' to 'S'"

as a compilation error, with the additional note

"Constructor for struct 'S' is declared 'explicit'".

According to C++17's initialization rules (Explanation of point 3) S's copy constructor should not be considered for the initialization of the S parameter of i_take_an_S; S(int, double) should rather be selected as an exact match by direct-list-initialization.

Might this be a bug in MSVC?

Jarod42
  • 203,559
  • 14
  • 181
  • 302
Ruperrrt
  • 489
  • 2
  • 13
  • The bug is not in the handling of `S{ 4, 2.0 }`. – Sam Varshavchik Sep 01 '22 at 12:30
  • MSVC might actually be the correct one here. That said, I could see guaranteed copy elision applying here meaning no copy happens. – NathanOliver Sep 01 '22 at 12:32
  • 1
    @NathanOliver I believe, MSVC is incorrect, since the standard guarantees copy/move elision here (returning prvalue and object initialization with prvalue). Moreover, if you swich standard to 11 in gcc or clang, it would complain about the explicit constructors. – trokymchuk Sep 01 '22 at 12:41
  • Just asking - why would you ever want an explicit copy or move constructor? Seems a crazy thing to even try. In my experience, `explicit` is there to avoid unwanted conversions from other types. (I know what it does and that it's legal, but have never wanted to use it). – Tony Delroy Sep 01 '22 at 12:47
  • 1
    @TonyDelroy I was just experimenting with the initialization rules of C++17 (or greater) for my own educational purposes. – Ruperrrt Sep 01 '22 at 12:48

2 Answers2

2

Yes, MSVC seems to be wrong here.

Generally, since C++17, the initialization rules are so that S{ 4, 2.0 } will directly initialize the parameter S s of the function. (mandatory copy elision)

There is however an exception. An implementation is allowed to introduce a copy in a function parameter or a return value if the class type has only deleted or trivial copy/move constructors and destructor (and at least one of the former non-deleted).

That you declare the copy and move constructor explicit doesn't change that they are copy/move constructors. Because you are not using = default to define them, they are not trivial. Therefore the special permission does not apply and it is wrong of MSVC to try to perform a copy.

Furthermore this special kind of copy ignores accessibility and overload resolution and therefore explicit shouldn't be relevant even if it was performed, see [class.temporary]/3.


When exactly copy elision is performed affects the ABI however, so if this is a defect in MSVC's ABI, then it might not be easily fixed.

user17732522
  • 53,019
  • 2
  • 56
  • 105
1

Might this be a bug in MSVC?

Yes, MSVC is wrong in rejecting the code.

The operand S{ 4, 2.0 } of the return statement return S{ 4, 2.0 } is a prvalue of type S and from C++17 onwards due mandatory copy elison, the parameter named s of function i_take_an_S is constructed directly(without using any copy/move ctor) from S{ 4, 2.0 }.


This can be seen from copy elison:

Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible:

In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:

T f()
{
   return T();
}

f(); // only one call to default constructor of T
Jason
  • 36,170
  • 5
  • 26
  • 60