C++ Constructor: Perfect forwarding and overload

2020-06-18 10:12发布

问题:

I have two classes, A and B, and B is derived from A. A has multiple constructors (2 in the example below). B has one additional member to initialize (which has a default initializer).

How can I achieve that B can be construced using one of the constructors of A without having to manually rewrite all the constructor overloads from A in B?

(In the example below, I would otherwise have to provide four constructors for B:B():A(){}, B(string s):A(s){}, B(int b):A(),p(b){}, B(string s, int b):A(s),p(b){}, instead of just two, at least when ignoring the possibility of default arguments).

My approach was perfect forwarding, however, the following scenario leads to an error:

#include <utility>
#include <string>

struct A {
    A(const std::string& a) : name(a) {}
    A(){}
    virtual ~A(){}

    std::string name;
};

struct B : public A {
    template<typename... Args>
    B(Args&&... args) : A(std::forward<Args>(args)...) {}

    B(const std::string& a, int b) : A(a), p(b) {}

    int p = 0;
};

int main()
{
    B b1("foo");
    B b2("foobar", 1);
}

For b2, GCC complains about no matching function for call to 'A::A(const char [5], int). Apparently it is trying to call the perfect forwarding constructor, which obviously shouldn't work, instead of the second constructor of B.

Why doesn't see the compiler the second constructor and call that instead? Are there technical reasons that the compiler can't find the correct constructor of B? How can I fix this behaviour?

The exact error message:

main.cpp: In instantiation of 'B::B(Args&& ...) [with Args = {const char (&)[5], int}]':
main.cpp:26:19:   required from here
main.cpp:15:54: error: no matching function for call to 'A::A(const char [5], int)'
     B(Args&&... args) : A(std::forward<Args>(args)...) {}
                                                      ^
main.cpp:6:5: note: candidate: A::A()
     A(){}
     ^
main.cpp:6:5: note:   candidate expects 0 arguments, 2 provided
main.cpp:5:5: note: candidate: A::A(const string&)
     A(const std::string& a) : name(a) {}
     ^
main.cpp:5:5: note:   candidate expects 1 argument, 2 provided
main.cpp:4:8: note: candidate: A::A(const A&)
 struct A {
        ^
main.cpp:4:8: note:   candidate expects 1 argument, 2 provided

回答1:

"foobar" is a const char (&) [7]. Therefore Args is a better match than a const std::string&

Thus, this overload is picked:

template<typename... Args>
B(Args&&... args) : A(std::forward<Args>(args)...) {}

where Args is const char (&) [7]

so it becomes:

B(const char (&&args_0) [7], int&& args_1)

which is forwarded to A's 2-argument constructor... which does not exist.

The wanted behavior is that if you construct a B with a constructor that works for A then the "...Args constructor" of B is called, otherwise another constructor of B gets called, otherwise it fails with "no appropriate constructor for B found".

something like this...

#include <utility>
#include <string>

struct A {
    A(std::string a) : name(std::move(a)) {}
    A(){}
    virtual ~A(){}

    std::string name;
};

template<class...T> struct can_construct_A
{
    template<class...Args> static auto test(Args&&...args)
    -> decltype(A(std::declval<Args>()...), void(), std::true_type());

    template<class...Args> static auto test(...) -> std::false_type;

    using type = decltype(test(std::declval<T>()...));
    static constexpr bool value = decltype(test(std::declval<T>()...))::value;
};

struct B : public A {

    template<class...Args>
    B(std::true_type a_constructable, Args&&...args)
    : A(std::forward<Args>(args)...)
    {}

    template<class Arg1, class Arg2>
    B(std::false_type a_constructable, Arg1&& a1, Arg2&& a2)
    : A(std::forward<Arg1>(a1))
    , p(std::forward<Arg2>(a2))
    {
    }

    template<typename... Args>
    B(Args&&... args)
    : B(typename can_construct_A<Args&&...>::type()
        , std::forward<Args>(args)...) {}

    int p = 0;
};

int main()
{
    B b1("foo");
    B b2("foobar", 1);
}

After seeing that A doesn't have a matching constructor, why doesn't it go back and continue looking for other constructors of B that might match? Are there technical reasons?

In a nutshell (and putting it very simply), when overload resolution takes place the compiler does the following:

  1. expand all templated overloads that can possibly match the arguments given. Add them to a list (with a weight indicating the level of specialisation that was involved in getting there).

  2. add any concrete overloads to the list that can be arrived at by legally apply conversion operators to the arguments, with a weight indicating how many conversions are required to turn the supplied arguments into the function argument types.

  3. sort the lists by ascending 'work' or weight.

  4. pick the one that requires least work. If there is a tie for which one is best, error.

The compiler gets one go at this. It's not a recursive search.

My apologies in advance to the purists amongst us who will find this childish explanation offensive :-)



回答2:

Option #1

Inherit constructors from class A:

struct B : A 
{
    using A::A;
//  ~~~~~~~~~^

    B(const std::string& a, int b) : A(a), p(b) {}

    int p = 0;
};

Option #2

Make B's variadic constructor SFINAE-able:

#include <utility>

struct B : A
{
    template <typename... Args, typename = decltype(A(std::declval<Args>()...))>
    //                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
    B(Args&&... args) : A(std::forward<Args>(args)...) {}

    B(const std::string& a, int b) : A(a), p(b) {}

    B(B& b) : B(static_cast<const B&>(b)) {}
    B(const B& b) : A(b) {}

    int p = 0;
};


回答3:

Without going into too much detail, the forwarding constructor is almost always preferred . It may even be preferred to the copy-constructor.

One technique to avoid this ambiguity is to make the caller explicitly select whether they want the forwarding constructor, by using a dummy parameter:

struct B : A
{
    enum dummy_t { forwarding };
    // ...

    template<typename... Args>
    B(dummy_t, Args&&... args) : A(std::forward<Args>(args)...) {}         
};

with sample usage:

B b2("foobar", 1);
B b(B::forwarding, "foobar");

Then you can even have an A and a B constructor with same parameters.


An alternative solution to your problem would be to write using A::A; in B's definition . This is like providing B with a set of constructors matching A's, and they initialize the A by calling the corresponding A constructor with the same arguments.

Of course this has some drawbacks, e.g. you can't also initialize other B members at the same time. Further reading