C++ Template Constructor

C++ template constructor

There is no way to explicitly specify the template arguments when calling a constructor template, so they have to be deduced through argument deduction. This is because if you say:

Foo<int> f = Foo<int>();

The <int> is the template argument list for the type Foo, not for its constructor. There's nowhere for the constructor template's argument list to go.

Even with your workaround you still have to pass an argument in order to call that constructor template. It's not at all clear what you are trying to achieve.

How to write a constructor for a template class using universal reference arguments in C++

In this snippet, d and s are NOT universal references - they are rvalue references (because D and S are already known (deduced) within class by the class instantiation).

 template <typename D, typename S>
struct MyClass {
constexpr MyClass(D &&d, S &&s): d_(std::forward<D>(d)), s_(std::forward<S>(s)) {}
private:
D d_;
S s_;
};

According to Scott Meyers: the type deduction needs to be involved for variables of type D&& and S&& (and in this form only!) for those to be universal references, so you need extra template parameters (so the types are deduced by the class-method(=ctor in this case) instantiation):

template <typename D, typename S>
struct MyClass {
template <typename DD, typename SS>
constexpr MyClass(DD &&d, SS &&s): d_(std::forward<DD>(d)), s_(std::forward<SS>(s)) {}
private:
D d_;
S s_;
};

Enabling class tempate type deduction via constructor call can be done via class template deduction guide like:

template <typename DD, typename SS> MyClass(DD &&d, SS &&s) -> MyClass<DD, SS>

Can class template constructors have a redundant template parameter list in c++20

There was a change, in fact. It's documented in the compatibility section of the C++20 draft.

[diff.cpp17.class]

2 Affected subclauses: [class.ctor] and [class.dtor]

Change: A simple-template-id is no longer valid as the declarator-id of a constructor or destructor.

Rationale: Remove potentially error-prone option for redundancy.

Effect on original feature: Valid C++ 2017 code may fail to compile in this International Standard. For example:

template<class T>
struct A {
A<T>(); // error: simple-template-id not allowed for constructor
A(int); // OK, injected-class-name used
~A<T>(); // error: simple-template-id not allowed for destructor
};

Specifically, the wording delta is this:

n4659 - C++17 standard draft - [class.ctor]

1 Constructors do not have names. In a declaration of a constructor,
the declarator is a function declarator of the form

ptr-declarator ( parameter-declaration-clause ) noexcept-specifier attribute-specifier-seq

where the ptr-declarator consists solely of an id-expression, an optional attribute-specifier-seq, and optional surrounding parentheses, and the id-expression has one of the following forms:

  • in a member-declaration that belongs to the member-specification of a class but is not a friend declaration, the id-expression is the injected-class-name of the immediately-enclosing class;
  • in a member-declaration that belongs to the member-specification of a class template but is not a friend declaration, the id-expression is
    a class-name that names the current instantiation of the
    immediately-enclosing class template; or

n4861 - C++20 standard draft - [class.ctor]

1 A constructor is introduced by a declaration whose declarator is a
function declarator ([dcl.fct]) of the form

ptr-declarator ( parameter-declaration-clause ) noexcept-specifier attribute-specifier-seq

where the ptr-declarator consists solely of an id-expression, an
optional attribute-specifier-seq, and optional surrounding
parentheses, and the id-expression has one of the following forms:

  • in a member-declaration that belongs to the member-specification of a class or class template but is not a friend declaration
    ([class.friend]), the id-expression is the injected-class-name
    ([class.pre]) of the immediately-enclosing entity or

As you can see, the wording changed. C++20 now requires the injected class name when declaring a constructor for a class template. S<T> is a simple template id that names a specialization. Inside a template, the injected class name is simply S.

This is part of addressing CWG 2237.

Conditional class template constructor

The problem with

// constructor for case when b is true
my_class(T n, int m) : n(n), m(m) {};

is that a constructor's mem-initializer-list can name only virtual base classes, direct base classes, and direct non-static data members, but never an inherited member like m. This is because the member of a base class is initialized by the base class subobject constructor, so it can't be initialized again (though it could be assigned).

You can instead specify the base class initializer. With this example, conditional_members is an aggregate, so aggregate initialization will work:

// constructor for case when b is true
my_class(T n, int m) : n(n), conditional_members<b>{m} {};

Though with just that, you might get some strange side effects from the fact that my_class specializations always have the two constructors declared, even if it might be invalid to actually instantiate one constructor or the other.

Here's an SFINAE trick to make the constructors conditionally effectively invisible, depending on b:

#include <type_traits>

// Define conditional_members as before.

template<typename T, bool b>
class my_class : public conditional_members<b>
{
T n;

public:
// constructor for case when b is false
template <typename = std::enable_if_t<!b>>
my_class(T n) : n(n) {}

// constructor for case when b is true
template <typename = std::enable_if_t<b>>
my_class(T n, int m) : conditional_members<b>{m}, n(n) {}
};

As a preview, with C++20 constraints, you'll be able to write it this nice, simpler way instead:

template<typename T, bool b>
class my_class : public conditional_members<b>
{
T n;

public:
// constructor for case when b is false
my_class(T n) requires(!b) : n(n) {}

// constructor for case when b is true
my_class(T n, int m) requires(b) : conditional_members<b>{m}, n(n) {}
};

Template class template constructor specialization

Your question is a little bit tricky to understand without an example. My understanding of it is that you want to specialize the constructor of a class template and construct it's member differently for different template arguments. If this isn't correct, let me know and I will adjust my answer.

Again without an example it's hard to know what you do and do not understand. But generally this is done the same way as you would specialize other methods. Declare all the specializations in your headers and implement them in your implementation file (unless they are partial specializations!). Remember to use template<> when specializing. Here is an example :

struct t_param_for_int {};
struct t_param_for_double {};


// t_member's constructor is very different depending on T
template<class T> struct t_member {};

template<> struct t_member<int> {
explicit t_member(const t_param_for_int value) {};
};

template<> struct t_member<double> {
explicit t_member(const t_param_for_double value) {};
};

// Foo has a t_member and a constructor
template<class T>
struct foo
{
foo();

t_member<T> member;
};

// Declare specialization
template<> foo<int>::foo();
template<> foo<double>::foo();

// Specialization implementations (in a .cpp)
template<> foo<int>::foo() : member(t_param_for_int{})
{ }

// Specialization implementations (in a .cpp)
template<> foo<double>::foo() : member(t_param_for_double{})
{ }

int main()
{
foo<int> int_foo;
foo<double> dbl_foo;
return 0;
}

Edit: In response to the edit to the question.

You cannot specialize a constructor in this case. The best solution is likely going to be to use a helper structs to do the actual initialization. You mentioned you want to initialize your containers with some number of 0 or default constructed elements T. However you didn't specify how large your containers should be. I've constructed an example to illustrate the solution of using helper structs using bogus container sizes.

#include <array>
#include <iostream>
#include <type_traits>
#include <string>
#include <utility>
#include <vector>

template<typename T, typename C>
struct helper_init;

template<typename T>
struct helper_init<T, std::vector<T>> {
static std::vector<T> init() {
return std::vector<T>(3, T{}); // init your vector with 3 elements
}
};

template<typename T>
struct helper_init<T, std::array<T, 2>> {
static std::array<T, 2> init() {
return {}; // init your array with 2 elements
}
};

template <typename T, typename C>
struct foo {
T m;
C container;
using value_type = typename C::value_type;

template <typename... Args>
foo(Args&&... args)
: m(std::forward<Args>(args)...)
, container(helper_init<T, C>::init())
{}
};

int main()
{
foo<int, std::vector<int>> vec_foo(5);
foo<std::string, std::array<std::string, 2>> str_foo("hello");

// Output to illustrate

// The size of the containers are 3 and 2 (see container initialization)
std::cout << vec_foo.container.size() << ' '
<< str_foo.container.size() << std::endl;

// The m members contain the assigned values
std::cout << vec_foo.m << " \'" << str_foo.m << '\'' << std::endl;

// The containers are zero or default initialized
std::cout << vec_foo.container.front() << " \'" <<
str_foo.container.front() << '\'' << std::endl;
return 0;
}

For the second part of your question, initializing to 0 to arithmetic types and default constructing for class types, the language already has a feature for this.

std::array specifically says this about it's construction.

initialized the array following the rules of aggregate initialization

Then aggregate initialization says this.

If the number of initializer clauses is less than the number of members or initializer list is completely empty, the remaining members are value-initialized.

Finally value initialization says this.

1) if T is a class type with at least one user-provided constructor of any kind, the default constructor is called;

4) otherwise, the object is zero-initialized.

This allow us to do this std::array<T, 10> my_array{}; and have ten zeroed or default constructed Ts. We can also do std::vector<T> my_vector(10, T{}); to get the same results (T{} is value constructed`).

Edit 2: Here is another solution that more adheres to the question's requirements using a [delegating constructor] and tag dispatching.

#include <array>
#include <string>
#include <vector>


// Tags for supported containers
struct t_tag_array {};
struct t_tag_vector {};

// Tag dispatching
template<class T, size_t S>
struct t_container_tag {};

template<class T, size_t S>
struct t_container_tag<std::vector<T>, S> {
using type = t_tag_vector;
};

template<class T, size_t S>
struct t_container_tag<std::array<T, S>, S> {
using type = t_tag_array;
};

// Helper to fetch the size of an std::array
template<typename>
struct array_size;

template<typename T, size_t S>
struct array_size<std::array<T, S>> {
static const auto value = S;
};

template <typename C, size_t S = array_size<C>::value>
struct foo
{
using value_type = typename C::value_type;

// Constructor
template<typename... Args>
foo(Args&&... args) : foo(typename t_container_tag<C, S>::type{}, std::forward<Args>(args)...) {}

// Specialized constructor for vectors
template<typename... Args>
foo(t_tag_vector &&, Args&&... args) : m(std::forward<Args>(args)...), container(S, value_type{}) {}

// Specialized constructor for arrays
template<typename... Args>
foo(t_tag_array &&, Args&&... args) : m(std::forward<Args>(args)...), container{} {}

value_type m;
C container;
};

Provide definition of constructor template outside a class template

The correct syntax is:

template <class T>
template <class S>
C<T>::C(const S& str)
{
// ...
}

Regarding your second question:

One example of using this pattern (of templating a ctor) is the way to achieve type-erasure in C++ using templates.

This article has some examples: https://quuxplusone.github.io/blog/2019/03/18/what-is-type-erasure/ (search for "templated constructor").

Or this 10 minutes clip that demonstrates it: https://www.youtube.com/watch?v=ZPk8HuyrKXU.

Struct template and constructor with initializer list

Both approaches are possible, initialization in C++ is tricky simply because of how many ways there are and their subtle differences.

I would recommend something like this:

#include <cstdint>
#include <utility>

template <typename T, std::size_t N> struct SVectorN {
SVectorN(const T (&another)[N]);

template <typename First, typename Second, typename... Tail>
SVectorN(First &&f, Second &&s, Tail &&...t)
: components{std::forward<First>(f), std::forward<Second>(s),
std::forward<Tail>(t)...} {}

private:
T components[N];
};

int main() {
double input[4] = {0.1, 0.2, 0.3, 0.4};
SVectorN<double, 4> t(input);

SVectorN<double, 4> c = {1.3, 1.4, 1.5, 1.6};
SVectorN<double, 4> d{1.3, 1.4, 1.5, 1.6};
SVectorN<double, 4> e(1.3, 1.4, 1.5, 1.6);
}

I had to explicitly enforce presence of at least two arguments in the variadic ctor, otherwise it would be a better overload match for t's construction and that would fail.

c,d,e are practically the same initialization if there is not ctor with std::initializer_list. Which there is not because initializing an array with one cannot be easily done.

Now, if you want to automatically deduce the template parameters, that is possible thanks to CTAD. To simplify that link, you have to just specify to the compiler how to deduce class's template arguments from a constructor's arguments.

For example the following:

template <class T, std::size_t N>
explicit SVectorN(const T (&)[N]) -> SVectorN<T, N>;
template <typename F, typename S, typename... Tail>
SVectorN(F &&, S &&, Tail &&...) -> SVectorN<F, sizeof...(Tail) + 2>;

will allow:

    SVectorN tx(input);
SVectorN cx = {1.3, 1.4, 1.5, 1.6};
SVectorN dx{1.3, 1.4, 1.5, 1.6};
SVectorN ex(1.3, 1.4, 1.5, 1.6);

Generic constructor template called instead of copy/move constructor

It isn't shadowing the copy and move constructors. It is just beating them in overload resolution in some cases.

  • If you pass a myTag<float>&, the forwarding constructor is used.
  • If you pass a const myTag<float>& the copy constructor is used.
  • If you pass a myTag<float>&&, the move constructor is used.
  • If you pass a const myTag<float>&&, the forwarding constructor is used.

The copy and move constructors are not templates, so they will win against a template of the same signature. But the forwarding constructor can be a better match in the cases where the deduced signature differs from the copy and move constructors.

The idiomatic way to handle this is to remove the forwarding constructor from consideration based on the decayed type of the arguments:

template <typename... Args>
constexpr myTag(Args&&... args)
requires
((sizeof...(Args) != 1 && std::is_constructible_v<T, Args...>) ||
(sizeof...(Args) == 1 && std::is_convertible_v<Args..., T> && !std::is_same_v<myTag, std::decay_t<Args>...>))


Related Topics



Leave a reply



Submit