Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variadic template constructor selection fails when argument is a reference

I have the following code:

#include <iostream>
#include <typeinfo>

template <typename T>
struct A : T {
    template <typename ...Args>
    A(Args&&... params) : T(std::forward<Args>(params)...), x(0) {
        std::cout << "Member 'x' was default constructed\n"; 
    }

    template <typename O, typename ...Args, typename = typename std::enable_if<std::is_constructible<int,O>::value>::type>
    A(O o, Args&&... params) : T(std::forward<Args>(params)...), x(o) {
        std::cout << "Member 'x' was constructed from arguments\n"; 
    }

    int x;
};

struct B{
    B(const char*) {}
};

int main() {
    A<B> a("test");
    A<B> y(3, "test");

    return 0;
}

It works fine, and prints

Member 'x' was default constructed
Member 'x' was constructed from arguments

However, if the first argument of the second overload is a reference, suddenly the second overload is never taken, and compilation fails:

template <typename O, typename ...Args, typename = typename std::enable_if<std::is_constructible<int,O>::value>::type>
    A(O& o, Args&&... params) : T(std::forward<Args>(params)...), x(o) {
        std::cout << "Member 'x' was constructed from arguments\n"; 
    } // Note the O& in the arguments

Why is this? Is it possible to fix it and avoid copies?

EDIT: Using an universal reference apparently makes it work again. A const reference, which is what I'd actually like, does not work either.

In addition, even saving the input parameter into a separate value (avoiding an rvalue) will still not work:

int main() {
    double x = 3.0;
    A<B> y(x, "test"); // Still not working

    return 0;
}
like image 260
Svalorzen Avatar asked Feb 11 '23 23:02

Svalorzen


1 Answers

Why is this?

In case of the following declaration:

template <typename O>
A(O& o);

the call:

A{3};

deduces the O type to be int, hence you end up with the following instantiation:

A(int& o);

But what you are doing, is you are trying to bind an rvalue (which 3 certainly is) to this instantiated non-const lvalue reference, and this is not allowed.

Is it possible to fix it and avoid copies?

You can declare the o type to be a forwarding reference as well, and then forward it to the constructor of x (but for primitive types like int this is really not necessary at all):

template <typename O>
A(O&& o) : x{std::forward<O>(o)} {}

Alternatively, you can declare the constructor as taking a const lvalue reference (so that rvalues can be bound by it):

template <typename O>
A(const O& o) : x{o} {}

Using a universal reference fixes the problem, but a const reference (which is actually what I wanted) does not, unfortunately. In addition, even saving the input parameter into a separate value (avoiding an rvalue) will still not work.

This is because a universal reference almost always produces an exact match, and the first constructor taking universal references is the best viable function in the overload resolution procedure.

When passing an rvalue, the deduced int&& is a better match for rvalues than const int&.

When passing an lvalue, the deduced int& is a better match for non-const lvalues (like your variable x) than const int&.

Having said that, this greedy constructor taking universal references is in both cases the best viable function, because when instantiating:

template <typename... Args>
A(Args&&... params);

template <typename O, typename... Args>
A(const O& z, Args&&... params);

e.g. for the following call:

double x = 3.0;
A a(x, "test");

the compiler ends up with:

A(double&, const char (&)[5]);

A(const double&, const char (&)[5]);

where the first signature is a better match (no need to add a const qualification).

If for some reasons you really want to have this O type to be templated (now no matter if this will be a universal reference or a const lvalue reference), you have to disable the first greedy constructor from the overload resolution procedure if its first argument can be used to construct int (just like the second one is enabled under such conditions):

template <typename T>
struct A : T
{
    template <typename Arg, typename... Args, typename = typename std::enable_if<!std::is_constructible<int, Arg>::value>::type>
    A(Arg&& param, Args&&... params) : T(std::forward<Arg>(param), std::forward<Args>(params)...), x(0) {
        std::cout << "Member 'x' was default constructed\n"; 
    }

    template <typename O, typename... Args, typename = typename std::enable_if<std::is_constructible<int, O>::value>::type>
    A(const O& o, Args&&... params) : T(std::forward<Args>(params)...), x(o) {
        std::cout << "Member 'x' was constructed from arguments\n"; 
    }

    int x;
};

DEMO

like image 179
Piotr Skotnicki Avatar answered Feb 14 '23 16:02

Piotr Skotnicki