1

Say I have this code:

#include <string>
using namespace std::literals;

struct A {
    A(std::string) { };
};

struct B : A {  };

And then construct objects like this:

A a("foo");
B b("foo"s);    // fails in clang
B c(A("foo"));  // fails in clang
B d("foo");     // fails in clang + gcc
B e({"foo"});   // fails in clang

A f{"foo"};
B g{"foo"s};
B h{A{"foo"}};
B i{"foo"};     // fails in clang + gcc
B j{{"foo"}};

How are a, b, c, ... actually constructed? What conversion, initialization method, constructor actually gets called? And why does it fail?

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
  • @273K I don't want to make more fail. I want to know how it works. How is it copy constructing `std::string` -> `B`? A `std::string` is not a `B`. Something else has to happen (first). – Goswin von Brederlow May 27 '22 at 01:29
  • @user4581301 Where would there be slicing? I'm not assigning a derived object to a base class. – Goswin von Brederlow May 27 '22 at 01:30
  • Yep Got `A` and `B` reversed in my head. Shutting up now. – user4581301 May 27 '22 at 01:31
  • All 3 fail by me https://godbolt.org/z/YEx7azcnP And `using std::literals;` does not compile. It seems that code is far from a [mcve]. – 273K May 27 '22 at 01:32
  • @273K Interesting. Works with `gcc -std=c++20` but fails with `clang -std=c++20`. That raises the question: which compiler is right? Fixed the missing `namespace`. https://godbolt.org/z/h6h1fhTh1 – Goswin von Brederlow May 27 '22 at 01:38
  • `gcc -std=c++17` is also failing. `gcc -std=c++20 -ansi` is also failing. So, I assume it is some of new gcc extensions. – 273K May 27 '22 at 01:40
  • @GoswinvonBrederlow The difference between GCC and Clang here is the fact that you're using `()` to initialize an aggregate, which is a C++20 feature, that Clang doesn't support yet. However, that seems tangential to your question, so I suggest using `{}` to initialize the variables. That way, all compilers show the same behavior described in the question, and doesn't need C++20 to compile. Also, are you sure you're looking for a [language-lawyer] answer? – cigien May 27 '22 at 02:29
  • https://en.cppreference.com/w/cpp/language/aggregate_initialization says an aggregate has *no user-declared or inherited constructors*. `B` inherits a user-declared constructor from `A` so it's not an aggregate. But `is_aggregate() == true` in gcc. – Goswin von Brederlow May 27 '22 at 02:34
  • Constructors are not inherited automatically, so while `A` is not an aggregate type due to the user-defined constructor, `B` actually is an aggregate type https://godbolt.org/z/87M4EM1M4 – cigien May 27 '22 at 02:42
  • @cigien Ok, that makes sense. Both gcc and clang agree that `B` is an aggregate. And with `{}` both will do aggregate initialization. That means `b` will call the user-defined constructor `A(std::string)` and `c` calls the copy constructor `A(const A&)`. But for the implicit `const char *` -> `std::string` that happens for `a` to happen for `d` I have to write `B d{{"foo"}};` so `B::A` gets initialized with initializer-list. Correct? – Goswin von Brederlow May 27 '22 at 02:50
  • Yes, that sounds about right. Regardless, I'd suggest changing the initialization in your question to use `{}`. Then you can also mention the question in your previous comment (i.e. whether `{{}}` does what you think). It's better to have a clean question that can get answers, instead of having the answer in comments, which can get deleted at any time. – cigien May 27 '22 at 02:53

1 Answers1

1

cigien pointed out that B doesn't automatically inherit any constructor from A and is an aggregate type. So aggregate initialization happens for B. And aggregate initialization with () is a c++20 feature that clang does not yet support. That explains all the differences between gcc and clang.

  • a and f do an implicit conversion from const char * to std::string before calling A(std::string).

  • b and g call A(std::string) during aggregate initialization

  • c and h call A(const A&) during aggregate initialization

  • d and i fail to find a const char * -> A conversion for aggregate initialization

  • e and h call A(const std::initializer_list &) which in turn uses A(std::string) to initialize the B::A

d and i bugs me. Feels inconsistent. If I can construct A from const char * then why not there? But that's C++.

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
  • I think this is related to the fact that your constructor is not declared `explicit` and thus compiler is allowed to make only [one implicit conversion](https://stackoverflow.com/a/121163) to resolve the argument, not two conversions. – heap underrun May 27 '22 at 07:27
  • @heapunderrun explicit would reduce that to 0 implicit conversions. I don't think there is anything to extend this to 2. – Goswin von Brederlow May 27 '22 at 12:57