Francis Cugler
Francis Cugler

Reputation: 7905

Ensuring template argument type matches that of its variadic constructor

I would like to have a class like this:

template<typename T>
struct Foo {
    T* data_;

    template<typename... Ts, std::enable_if<std::is_same<T,Ts>...>...>
    explicit Foo(Ts...ts) : data_{ ts... } {}
};

However; something with the syntax is wrong, and I'm not sure if you can set parameters into a pointer directly like this upon initialization.

What I would like for this to do is simply this:

Foo<int> f1{ 1, 3, 5, 7 }; // Or
// Foo<int> f1( 1, 3, 5 7 );
// f1.data_[0] = 1
// f1.data_[1] = 3
// f1.data_[2] = 5
// f1.data_[3] = 7
// f1.data_[4] = ... not our memory either garbage or undefined...

Foo<float> f2{ 3.5f, 7.2f, 9.8f }; // Or
// Foo<float> f2( 3.5f, 7.2f, 9.8f );
// f2.data_[0] = 3.5
// f2.data_[1] = 7.2
// f2.data_[2] = 9.8
// f2.data_[3] = ... not our memory

I would also like to have the constructor check to make sure that each and every parameter that is passed into the constructor is of type <T>; simply put for each Ts it must be a T.

I might be overthinking this but for the life of me I can not get this or something similar to compile. I don't know if it's within enable_if, is_same or through the class's initializer list and trying to store the contents into a pointer. I don't know if I should use an array of T instead but the array's size won't be known until the arguments are passed into the constructor. I'm also trying to do this without using a basic container such as std::vector; it's more for self education than practical source code. I just want to see how this could be done with raw pointers.


Edit

I've changed my class to something like this:

template<typename T>
struct Foo {
    T* data_;

    template<typename... Ts, std::enable_if_t<std::is_same<T, Ts...>::value>* = nullptr>
    explicit Foo( const Ts&&... ts ) : data_{ std::move(ts)... } {}
 };

And when trying to use it:

 int a = 1, b = 3, c = 5, d = 7;
 Foo<int> f1( a, b, c, d );
 Foo<int> f2{ a, b, c, d };

I'm a little closer with this iteration; but they both give different compiler errors.

Upvotes: 2

Views: 85

Answers (4)

Picaud Vincent
Picaud Vincent

Reputation: 10982

Why not simply use a std::initialize_list:?

#include <iostream>
#include <type_traits>
#include <vector>

template <class T>
struct Foo
{
  std::vector<T> data_;

  explicit Foo(std::initializer_list<T> data) : data_(data)
  {
    std::cout << "1";
  };

  template <typename... Ts,
            typename ENABLE=std::enable_if_t<(std::is_same_v<T,Ts> && ...)> >
  explicit Foo(Ts... ts) : Foo(std::initializer_list<T>{ts...})
  {
    std::cout << "2";
  }
};

int main()
{
  Foo<int> f1{1, 3, 5, 7}; // prints 1
  Foo<int> f2(1, 3, 5, 7); // prints 1 then 2

  return 0;
}

If some Ts are different from T you will get a compile-time error.

With

gcc -std=c++17  prog.cpp  

you get:

  Foo<int> f1{1, 3, 5., 7};

error: narrowing conversion of ‘5.0e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing] Foo f1{1, 3, 5., 7}; ^

and

Foo<int> f2(1, 3, 5., 7);

you get

error: no matching function for call to ‘Foo::Foo(int, int, double, int)’ Foo f2(1, 3, 5., 7); ^ note: candidate: ‘template Foo::Foo(Ts ...)’ explicit Foo(Ts... ts) : Foo(std::initializer_list{ts...})

...

Update: if you really want to use something like raw pointer, here is a complete working example:

#include <iostream>
#include <memory>
#include <type_traits>
#include <vector>

template <class T>
struct Foo
{
  size_t n_;
  std::unique_ptr<T[]> data_;

  explicit Foo(std::initializer_list<T> data) : n_(data.size()), data_(new T[n_])
  {
    std::copy(data.begin(), data.end(), data_.get());
    std::cout << "1";
  };

  template <typename... Ts, typename ENABLE = std::enable_if_t<(std::is_same_v<T, Ts> && ...)> >
  explicit Foo(Ts... ts) : Foo(std::initializer_list<T>{ts...})
  {
    std::cout << "2";
  }

  friend std::ostream& operator<<(std::ostream& out, const Foo<T>& toPrint)
  {
    for (size_t i = 0; i < toPrint.n_; i++)
      std::cout << "\n" << toPrint.data_[i];
    return out;
  }
};

int main()
{
  Foo<int> f1{1, 3, 5, 7};  // prints 1
  Foo<int> f2(1, 3, 5, 7);  // prints 1,2

  std::cout << f1;
  std::cout << f2;

  return 0;
}

I let you replace unique_ptr by a raw pointer with all the extra work: delete[] etc...

Upvotes: 4

bartop
bartop

Reputation: 10315

The most idiomatic way to do this in C++17 is using std::cunjunction_v. It lazily evaluates subsequent values and allows you to avoid fold expressions. Also messages generated by the compiler are the same for both of casese you mentioned in edited piece of code.

Also, passing data by const-rvalue-ref makes no sense, since it is impossible to move data from const objects. Additionaly you were moving pack of arguments to a pointer. I didn't know what to do about it so just removed it.

Additionaly, take a look at the std::decay_t - without it it would not work, as sometimes Ts is deduced not as int but as const int & or int &. This leads to std::is_same_v being false.

The code below compiles fine at godbolt:

template<typename T>
struct Foo {
    T* data_;

    template<
        typename... Ts,
        std::enable_if_t<
            std::conjunction_v<
                std::is_same<T, std::decay_t<Ts>>...
            >
        > * = nullptr
    >
    explicit Foo( Ts&&... ts ) : data_{ } {}
 };

Upvotes: 0

Miles Budnek
Miles Budnek

Reputation: 30494

std::is_same only compares two types, and you can't use pack expansions to declare multiple template parameters. That means you'll need to pull all of your std::is_same checks out into another check:

template <typename T, typename... Ts>
struct all_same : std::bool_constant<(std::is_same<T, Ts>::value && ...)> {};

template <typename T>
struct Foo
{
    std::vector<T> data_;

    template <typename... Ts, std::enable_if_t<all_same<T, std::decay_t<Ts>...>::value>* = nullptr>
    Foo(Ts&&... ts)
        : data_{std::forward<Ts>(ts)...}
    {
    }
};

Live Demo

You also need to allocate memory for your data_ array. Here I've used std::vector to take care of that allocation for me, but you could use new[] and delete[] to manage it yourself if you really want to.

Upvotes: 2

Rtbo
Rtbo

Reputation: 230

enable_if and is_same won't store anything anywhere, they are only compile-time constructs and do not yield to any code in the binary executable.

Regardless of the syntax, what your code is essentially doing is trying to take the address of a constructor argument (which is a temporary). This will be a dangling pointer as soon as the constructor exits.

Either Foo owns the memory area and must allocate in constructor and delete in destructor (if any doubt: use std::vector!), or it aliases some external memory, and must receive a pointer to that memory.

Now regarding syntax:

  • std::is_same is a template that provides a value boolean constant and is to be used like so: std::is_same<T1, T2>::value. Alternatively you can use std::is_same_v<T1, T2>.
  • std::enable_if provides a type type member, only if the constant expression (1st template parameter) is true. Use it like std::enable_if<expr, T>::type. If expr is true, type is a typedef to T. Otherwise it is not defined and yields a substitution failure. Alternatively you can use std::enable_if_t<expr, T>

You can have a look here for a similar approach of yours.

But you can also simplify all this by using a member vector. In that case, the vector constructor ensures that all arguments have compatible types. Here is a complete example:

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

using namespace std;

template<typename T>
struct Foo {
    vector<T> data_;

    template<typename ...Ts>
    explicit Foo(Ts... ts) : data_{ ts... } {}

    void print() {
        for (const auto &v : data_) {
            cout << v << " ";
        }
        cout << endl;
    }
};

int main() {

    Foo<int> ints { 1, 2, 3, 4, 5 };
    Foo<string> strings { "a", "b", "c", "d", "e"};
    // Foo<string> incorrect { "a", 2, "c", 4, "e"};

    ints.print();
    strings.print();
    // incorrect.print();

    return 0;
}

Upvotes: 1

Related Questions