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;
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With