Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Constructor: Perfect forwarding and overload

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
like image 774
mrspl Avatar asked Apr 02 '16 08:04

mrspl


2 Answers

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;
};
like image 133
Piotr Skotnicki Avatar answered Oct 06 '22 02:10

Piotr Skotnicki


"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 :-)

like image 41
Richard Hodges Avatar answered Oct 06 '22 02:10

Richard Hodges