4

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;
}
Svalorzen
  • 5,353
  • 3
  • 30
  • 54

1 Answers1

8

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

Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160
  • Using an universal reference fixes the problem, but a `const` reference (which is actually what I wanted) does not, unfortunately. http://ideone.com/cU0pRQ as proof – Svalorzen Sep 23 '14 at 17:43
  • @Svalorzen: how about now? – Piotr Skotnicki Sep 23 '14 at 18:09
  • @Svalorzen: but honestly, I don't why why you want the first argument to be `const O& o` rather than simple `int o` – Piotr Skotnicki Sep 23 '14 at 18:12
  • Because for example it could be a very big heavy type which has an `operator int`. Or maybe I want to use a double, or an unsigned, and so on. I don't feel like your solution works, because then you would effectively need to enumerate all possible types you do not want, which you really can't do. – Svalorzen Sep 23 '14 at 18:15
  • @Svalorzen "I don't feel like your solution works", which solution? the presented one or the proposed in comments one? – Piotr Skotnicki Sep 23 '14 at 18:16
  • because the first constructor is the best viable function, as it produces better match, resulting in `A(int&&, const char (&)[5])`, which for `A(3, "test")` is better than `A(const int&, const char (&)[5])` – Piotr Skotnicki Sep 23 '14 at 18:22
  • You need to disable the first constructor in the case that the argument can be used to construct an int: universal references are 'greedy' and will bind to anything. – mattnewport Sep 23 '14 at 18:33
  • @Svalorzen: just remember that in `template A(Args&&...)` the `&&` does not indicate rvalue reference, it always produces exact match to what type the argument is – Piotr Skotnicki Sep 23 '14 at 18:36
  • Yes I understand. So the idea is that the other match is taken only if the first match is truly perfectly "matchable", otherwise it will fall back on the first one at the first sign of difference. Correct? – Svalorzen Sep 23 '14 at 18:45
  • @Svalorzen: unless you have already declared a function which better matches the arguments' types than what would result after deducing types of universal references, then compiler will go with universal references. That is, if you had e.g. `A(int, Args&&... params)`, the compiler would choose this instead of unviersal references (as the latter would result with either `A(int&&, Args&&... params)` for rvalues or `A(int&, Args&&... params)` for lvalues +const/volatile variations ), which all ARE worse than plain `A(int, Args&&...)` – Piotr Skotnicki Sep 23 '14 at 18:55
  • 1) `int &&` isn't worse than `int`. The first constructor template is picked because it's more specialized by partial ordering. 2) The `A(Args&&...)` constructor needs to be constrained so that it won't be used in place of the copy/move constructors when you pass an `A`. – T.C. Sep 24 '14 at 04:39
  • @T.C. ah yes, I should have said: "unless you have already declared a function which better *or equally good* matches the arguments' types than what would result after deducing types of universal references [...]". thanks – Piotr Skotnicki Sep 24 '14 at 12:51