Preference of conversion operator over copy constructor changes from C++14 to C++17?
Asked Answered
G

1

18

I was playing with the following code.

#include <iostream>

struct To
{
    To() = default;
    To(const struct From&) {std::cout << "Constructor called\n";}
    To(const To&) {std::cout << "Copy constructor called\n";}
};

struct From
{
    operator To(){std::cout << "Conversion called\n"; return To();}
};

int main()
{
    From f;
    To t(f);
    To t1 = f;
    To t2{To(f)};
}

If I use -std=c++14 both GCC and Clang agree to the following output.

Constructor called
Conversion called
Constructor called

However, If I compile with -std=c++17, both compilers agree to

Conversion called
Conversion called
Conversion called

I understand that the reduction of several-expected output lines to output 3 lines is due to copy elision, but I can't figure out what changed in C++17 that resulted in this output. What change exactly in standard did initiate this?

Grained answered 24/4, 2023 at 20:28 Comment(4)
The simple reason is in C++17, calling To(const struct From&) requires adding const, which is slightly worse than doing nothing to call operator To(). If you had had operator To() const or To(struct From&), it would be ambiguous. And in C++14, the conversion operator is usually worse because it would be conversion operator -> copy constructor (even if it ends up elided). Unsure why To t1 = f calls the conversion operator in that case.Diary
I can't figure out what changed in C++17 that resulted in this output And how far in [dcl.init] and [over] you've managed to get?Sangraal
@LanguageLawyer Not far enough I guess?Grained
Does this answer your question? Call to conversion operator instead of converting constructor in c++17 during overload resolutionEthban
S
11

Direct-initialization of an object of class type, in all versions of C++, performs overload resolution among the constructors of the class; see for example [dcl.init.general]/16.6.2 from the current draft. There are some exceptions, but they're not applicable here. It's important to note that conversion functions are not candidates for the initialization, so the compiler has to choose whether to use To::To(const To&) or To::To(const From&) for the initialization. The latter wins overload resolution because the parameter type has an exact match with the argument type.

The interesting thing is that if To::To(const From&) were not present, then To::To(const To&) would be called, and the From object's conversion function would be called in order to initialize the const To& parameter. This is not great, because it forces a copy to occur; since From::operator To directly creates a To object, it would be better if From::operator To directly initialized the target of the initialization, rather than creating a temporary object that has to be copied to the target. Unfortunately, the current standard requires the copy to occur. This is the topic of CWG2327.

The committee has not yet decided how to resolve CWG2327, but Clang and GCC have each implemented a different tweak to their overload resolution rules in order to allow copies to be elided under some circumstances. So basically, their behaviour in C++17 mode doesn't conform to the standard, but is "optimistic", i.e., the implementors hoped that the resolution to CWG2327 would be something resembling the tweaks they implemented.

You can read more detail here. An updated version of this paper, which will propose a formal resolution to CWG2327, will be in the next mailing.

Seng answered 25/4, 2023 at 1:39 Comment(4)
The link CWG2327 requires some login/password to proceed.Ethban
Just to make sure I understand correctly. In the document you have mentioned it says that To::To(const From&) will not prevail to conversion operator(hence copy-constructor) because the latter is less cv-qualified. I will assume this is true after C++17, because in the answer you said that To::To(const From&) wins the overload resolution due to exact matching. But the standard requires a copy to occur and To::To(const To&) should be picked because of that.Grained
@Ethban you can use cplusplus.github.io/CWG/issues/2327.html in the meantime.Seng
@KarenBaghdasaryan In standard C++, a constructor is selected for direct-initialization. As I said in my answer, To::To(const From&) gives an exact match so it is selected. The behaviour of calling the conversion function (and skipping the constructor) in C++17 mode is non-standard and P2828R0 explains the logic that Clang and GCC use in such cases.Seng

© 2022 - 2024 — McMap. All rights reserved.