I'm implementing something where I'd like to "nest" number types. I'm struggling to write constructors in a way that the right-hand side can be a numeric literal. Here's a minified version of my issue:
template <typename Scalar>
class A
{
public:
A(Scalar a):v(a){};
Scalar v;
};
template <typename Scalar>
class B
{
public:
B(Scalar a):v(a){};
Scalar v;
};
int main()
{
A<double> a = 1.0;
B<double> b = 1.0;
B<A<double>> ba = 1.0;
}
The compiler error is
error: no viable conversion from 'double' to 'B<A<double>>'
I don't want to write B<A<double>> b = A<double>(1.0);
. Is there a way to change my A
and B
classes so the original code works?
CodePudding user response:
What you want is a forwarding constructor. That is a constructor that takes the arguments to construct the wrapped object and calls that objects constructor with the supplied parameters. That would look like
template <typename Scalar>
class A
{
public:
A(Scalar a):v(a){};
Scalar v;
};
template <typename Scalar>
class B
{
public:
B(Scalar a): v(a) {}
template <typename... Args,
std::enable_if_t<std::is_constructible_v<Scalar, Args...>, bool> = true>
B(Args&&... args) : v(std::forward<Args>(args)...) {}
Scalar v;
};
int main()
{
A<double> a = 1.0;
B<double> b = 1.0;
B<A<double>> ba = 1.0;
}
The std::enable_if_t<std::is_constructible_v<Scalar, Args...>, bool> = true
part of the template leverages SFINAE to constrain this constructor to only work when it would be able to construct a Scalar
. This is needed because variadic templates are greedy and will match almost anything, including being considered a better constructor then the copy constructor when provided with a non-const lvalue of the class type.
CodePudding user response:
@NathanOliver mentioned using concepts and constraints with C 20 in the comment of his answer, so here's how you can do it with C 20 constraint:
template <typename Scalar>
struct B
{
public:
template <typename ... Args>
requires std::constructible_from<Scalar, Args...>
B(Args ... args) : v(std::forward<Args>(args) ...) {}
Scalar v;
};
Attempting to construct a B<A<double>>
from something that cannot convert to double
will create a compiler error:
B<A<double>> ba = "ok"; // Compiler error
// candidate template ignored: constraints not satisfied [with Args = <const char *>]
// because 'std::constructible_from<A<double>, const char *>' evaluated to false
Note, this doesn't really have an equivalent form that can be written as a concept directly:
template<std::constructible_from<Scalar> ... Args>
Would be the equivalent of, which is wrong in multiple ways:
template<typename Args_1, typename Args_2>
requires std::constructible_from<Args_1, Scalar> && std::constructible_from<Args_2, Scalar>
However, if you know you will only need constructor with a single parameter, then you can potentially use std::convertible_to
:
template <typename Scalar>
struct B
{
public:
template <std::convertible_to<Scalar> Arg>
B(Arg arg) : v(std::forward<Arg>(arg)) {}
Scalar v;
};
This do require an implicit conversion from Arg
to Scalar
however, so either the constructor of Scalar
, or a conversion function from Arg
to Scalar
must not be explicit.
Side note, the base constructor B(Scalar a): v(a) {}
at this point is basically redundant, as it will be covered by the templated constructor.