Home > Back-end >  How to write constructors for nested template numeric type classes in C ?
How to write constructors for nested template numeric type classes in C ?

Time:04-04

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.

  • Related