Template class template constructor specialization

4k views Asked by At

I have a template class with a member whose type depends on the class' template argument. The class has a template constructor. How do I specialize the constructor for different cases of the class' template argument that determines the type of the said member. The different types in question are classes with different constructor signatures, and I'd like to call the member's constructor in the initializer list. Is there a workaround? I would like to avoid resorting to factory functions, because I don't want to rely on the move constructor of the member being cheap.

Edit: Here's a code example,

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(/*different stuff here*/)
  {}
};

My goal is to properly initialize the container regardless of whether C is a std::vector or std::array. If std::is_arithmetic<value_type>::value==true, I want to initialize the container with all zeros (this is where the issue with different constructor signatures comes in). If std::is_arithmetic<value_type>::value==false, I want to default initialize.

1

There are 1 answers

8
François Andrieux On BEST ANSWER

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;
};