3

I was trying to overload the casting operator in C++ for practice, but I encountered a problem and I can't figure out the issue. In the example, you can implicitly cast fine, but it causes an error when you try to explicitly cast.

struct B
{
    B() = default;
    B( B& rhs ) = default;
};

struct A
{
    operator B()
    {
        return B();
    }
};

int main()
{
    A a;
    B example = a;    //fine
    B example2 = static_cast<B>(a);    //error
}

The error is:

error C2440: 'static_cast': cannot convert from 'A' to 'B'

message : No constructor could take the source type, or constructor overload resolution was ambiguous

The problem only appears if you define the copy constructor in the B structure. The problem goes away, though, if you define the move constructor, too, or make the copy constructor take in a const B& ( B( const B& rhs ) ).

I think the problem is that the explicit cast is ambiguous, but I don't see how.

I was looking at a similar problem, here, but in that case I could easily see how the multiple options for casting led to ambiguity while I can't here.

Flyer924
  • 33
  • 3
  • 4
    Never define a copy ctor with a non-const reference parameter. Why do this? Where is such a thing advised or taught or whatever? – n. m. could be an AI Sep 11 '22 at 19:51
  • I was practicing overloading the cast operator and I forgot to make the copy constructor take a const reference. When I saw that it wasn't working, though (before I fixed it), I wanted to know why. – Flyer924 Sep 11 '22 at 19:57
  • `B example = a;` doesn't work either under the same conditions and for the same reasons that `B example2 = static_cast(a);` doesn't work. – user17732522 Sep 11 '22 at 20:43
  • @user17732522 `B example = a;` does compile and run for me (with the msvc compiler in visual studio), or did you mean something else? – Flyer924 Sep 11 '22 at 21:24
  • 1
    @Flyer924 That's MSVC being non-standard-conforming. If you enable standards conformance mode (`/permissive-`), then it will also correctly fail to compile. Note however that both lines will compile in C++17 or later due to changes in the initialization rules (mandatory copy elision). – user17732522 Sep 11 '22 at 21:31
  • 1
    There is no such thing as an implicit cast. A cast is something you write in your source code to tell the compiler to do a conversion; it’s always explicit. There also is no cast operator. The terms you’re looking for are “implicit conversion” and “conversion operator”. – Pete Becker Sep 12 '22 at 02:25

2 Answers2

4
static_cast<B>(a);

This expression is an rvalue, more loosely described as a temporary value.

The B class has no suitable constructor. The B( B &rhs) constructor is not suitable, mutable lvalue references don't bind to temporaries, hence the compilation failure.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • Question (sorry If I'm misunderstanding), but why does the explicit cast work if you remove the copy constructor from B? – Flyer924 Sep 11 '22 at 19:54
  • 1
    Because the default copy constructor's parameter is a `const` reference. Your explicitly-declared constructor deletes the default one, and your constructor's mutable reference parameter will not work. – Sam Varshavchik Sep 11 '22 at 19:55
  • That makes so much sense! Thank you! – Flyer924 Sep 11 '22 at 19:58
2

Before C++17:

Both lines do effectively the same thing. B example = /*...*/; is copy-initialization which will first convert the right-hand side to the type B if necessary in some suitable manner, resulting in a temporary object from which example is then initialized by choosing a suitable constructor (typically a copy or move constructor).

Because A is not related to B via inheritance, there is no way to bind a directly to the rhs parameter in the (copy) constructor of B. There must first be a conversion from a to a B which is possible implicitly or explicitly via the conversion function you defined.

The result of the conversion will always be a prvalue. A prvalue however can not be bound to a non-const lvalue reference. Therefore it doesn't help that the conversion is possible, the B( B& rhs ) constructor overload is still not viable.

A constructor B( const B& rhs ) (which is also the signature of the implicitly-declared copy constructor if you don't declare one yourself) would however be viable because the prvalue resulting from the conversion can be bound to a const lvalue reference.

Therefore both lines are ill-formed, before C++17.


Since C++17 the mandatory copy elision rules state that initialization of a variable of a type B from a prvalue of type B is done exactly as if the variable was directly initialized by the initializer of the prvalue, so that no overload resolution on the constructor will be performed at all. Whether there is a copy constructor or how exactly it is declared is then irrelevant. Similarly the rules for copy-initialization from a non-reference compatible lvalue such as a have been updated to have the same effect.

So since C++17 both lines will compile.


If you are using MSVC in a default configuration with the language standard set below C++17, the first line will compile, but that is not standard-conforming. By default when set to language versions below C++20 MSVC uses a slight dialect of C++ which is not conforming to the standard in some areas. You can set the standards conformance mode (/permissive- flag) to set MSVC to behave (more) standard-conforming.

user17732522
  • 53,019
  • 2
  • 56
  • 105