Different results between clang/gcc and MSVC for templated constructor in base class
Asked Answered
J

1

12

I stumbled over the following piece of code. The "DerivedFoo" case produces different results on MSVC than on clang or gcc. Namely, clang 13 and gcc 11.2 call the copy constructor of Foo while MSVC v19.29 calls the templated constructor. I am using C++17.

Considering the non-derived case ("Foo") where all compilers agree to call the templated constructor, I think that this is a bug in clang and gcc and that MSVC is correct? Or am I interpreting things wrong and clang/gcc are correct? Can anyone shed some light on what might be going on?

Code (https://godbolt.org/z/bbjasrraj):

#include <iostream>
using namespace std;

struct Foo {
  Foo() {
    cout << "\tFoo default constructor\n";
  }

  Foo(Foo const &) { cout << "\tFoo COPY constructor\n";
  }

  Foo(Foo &&) {
    cout << "\tFoo move constructor\n";
  }

  template <class U>
  Foo(U &&) {
    cout << "\tFoo TEMPLATED constructor\n";
  }
};

struct DerivedFoo : Foo {
   using Foo::Foo;
};

int main() {
  cout << "Foo:\n";
  Foo f1;
  Foo f2(f1);

  cout << "\nConst Foo:\n";
  Foo const cf1;
  Foo cf2(cf1);

  cout << "\nDerivedFoo:\n";
  DerivedFoo d1;
  DerivedFoo d2(d1);

  cout << "\nConst DerivedFoo:\n";
  DerivedFoo const cd1;
  DerivedFoo cd2(cd1);
}

Result for clang and gcc:

Foo:
    Foo default constructor
    Foo TEMPLATED constructor

Const Foo:
    Foo default constructor
    Foo COPY constructor

DerivedFoo:
    Foo default constructor
    Foo COPY constructor  <<<<< This is different

Const DerivedFoo:
    Foo default constructor
    Foo COPY constructor

Result for MSVC:

Foo:
        Foo default constructor
        Foo TEMPLATED constructor

Const Foo:
        Foo default constructor
        Foo COPY constructor

DerivedFoo:
        Foo default constructor
        Foo TEMPLATED constructor  <<<<< This is different

Const DerivedFoo:
        Foo default constructor
        Foo COPY constructor
Jacobsen answered 6/2, 2022 at 18:25 Comment(6)
If you use the /permissive- option to force c++17 compliance MSVC behaves properly. Looks like a holdover for old code that can break with conformant compilers.Insured
@Insured Are those behaviours (without permissive) publicly documented?Plagiarism
Don't know if that specific issue has been addressed. Microsoft has a long list of compliance differences and how to upgrade old code.Insured
@Insured I compiled the code in MSVC using /permissive- and got the above results (i.e. MSVC calls the templated constructor), both in C++17 and C++20. I used VS2019 (cl.exe in version 19.29.30139).Jacobsen
@Jacobsen The defect report seems to not be implemented prior to 19.30: godbolt.org/z/sjv6h74M4Smothers
I'm using 19.30. Just lucky.Insured
S
11

It is correct that the constructor template is generally a better match for the constructor call with argument of type DerivedFoo& or Foo& than the copy constructors are, since it doesn't require a const conversion.

However, [over.match.funcs.general]/8 essentially (almost) says, in more general wording, that an inherited constructor that would have the form of a move or copy constructor is excluded from overload resolution, even if it is instantiated from a constructor template. Therefore the template constructor will not be considered.

Therefore the implicit copy constructor of DerivedFoo will be chosen by overload resolution for

DerivedFoo d2(d1);

and this will call Foo(Foo const &); to construct the base class subobject.

This wording is a consequence of CWG 2356, which was resolved after C++17, but I think it is supposed to be a defect report against older versions as well. (I don't really know though.)

So GCC and Clang are correct here. Also note that MSVC behaves according to the defect report as well since version 19.30 if the conformance mode (/permissive-) is used.

Smothers answered 6/2, 2022 at 19:15 Comment(5)
Thanks! But what do you mean with "even if it is instantiated from a constructor template"? I mean, what does "it" refer to? Or, if I read the mentioned section in the C++ standard and what you wrote correctly, the templated constructor is explicitly excluded when it "looks like a copy constructor" once it is instantiated (Quote from the standard: "including such a constructor instantiated from a template")? But if the instantiated constructor does not "look like" a copy/move constructor, it is included in the set of candidate functions?Jacobsen
@Jacobsen "it" refers to "an inherited constructor". And yes, I think your interpretation is correct.Smothers
I see, thanks! Out of curiosity, do you happen to know the reasoning for CWG 2356, i.e. why these constructors are not inherited?Jacobsen
@Jacobsen Can't tell their motivation for sure, but I suppose that if you write DerivedFoo d2(d1); you expect the copy constructor of the derived class to be used and it would be surprising if some template constructor in the base that doesn't know about the derived class could override it. Note that having an unconstrained single-argument template constructor is already a problem in itself, since it messes with copy and move construction as in your Foo example. You should constrain it so that the template argument can't be Foo.Smothers
Yes, I planned to restrict the template constructor via something similar to std::enable_if_t<!std::is_same_v<std::remove_cv_t<std::remove_reference_t<U>>, Foo>> (this is how the std::optional implementations do it). But then I ran into the different results with MSVC and clang for DerivedFoo d2(d1): MSVC called the template constructor despite the enable_if because, well, DerivedFoo is not the same as Foo. I guess this case might be one of the reasons behind CWG 2356. Thanks again!Jacobsen

© 2022 - 2024 — McMap. All rights reserved.