Generating one class member per variadic template argument
Asked Answered
L

5

45

I have a template class where each template argument stands for one type of value the internal computation can handle. Templates (instead of function overloading) are needed because the values are passed as boost::any and their types are not clear before runtime.

To properly cast to the correct types, I would like to have a member list for each variadic argument type, something like this:

template<typename ...AcceptedTypes> // e.g. MyClass<T1, T2>
class MyClass {
    std::vector<T1> m_argumentsOfType1;
    std::vector<T2> m_argumentsOfType2; // ...
};

Or alternatively, I'd like to store the template argument types in a list, as to do some RTTI magic with it (?). But how to save them in a std::initializer_list member is also unclear to me.

Thanks for any help!

Limoges answered 14/1, 2015 at 11:18 Comment(5)
You can forward the argument types to a std::tuple.Saturant
Does it have to be separate members, or is okay to have a collection of vectors? Like e.g. an std::array of the vectors? [I don't know how to solve it either way, but your answer to this question might help those that do know how to solve this.]Gault
A collection is fine, too. Though I am not sure whether that would help. At some point the container (std::vector) just needs to know the type.Limoges
If only one of the vectors will be populated at any one time, why not use a boost::variant of vectors? Or will more than one be populated at a time?Excitation
See also: https://mcmap.net/q/374629/-c-push-multiple-types-onto-vectorPippin
M
34

As you have already been hinted, the best way is to use a tuple:

template<typename ...AcceptedTypes> // e.g. MyClass<T1, T2>
class MyClass {
    std::tuple<std::vector<AcceptedTypes>...> vectors;
};

This is the only way to multiply the "fields" because you cannot magically make it spell up the field names. Another important thing may be to get some named access to them. I guess that what you're trying to achieve is to have multiple vectors with unique types, so you can have the following facility to "search" for the correct vector by its value type:

template <class T1, class T2>
struct SameType
{
    static const bool value = false;
};

template<class T>
struct SameType<T, T>
{
    static const bool value = true;
};

template <typename... Types>
class MyClass
{
     public:
     typedef std::tuple<vector<Types>...> vtype;
     vtype vectors;

     template<int N, typename T>
     struct VectorOfType: SameType<T,
        typename std::tuple_element<N, vtype>::type::value_type>
     { };

     template <int N, class T, class Tuple,
              bool Match = false> // this =false is only for clarity
     struct MatchingField
     {
         static vector<T>& get(Tuple& tp)
         {
             // The "non-matching" version
             return MatchingField<N+1, T, Tuple,
                    VectorOfType<N+1, T>::value>::get(tp);
         }
     };

     template <int N, class T, class Tuple>
     struct MatchingField<N, T, Tuple, true>
     {
        static vector<T>& get(Tuple& tp)
        {
            return std::get<N>(tp);
        }
     };

     template <typename T>
     vector<T>& access()
     {
         return MatchingField<0, T, vtype,
                VectorOfType<0, T>::value>::get(vectors);
     }
};

Here is the testcase so you can try it out:

int main( int argc, char** argv )
{
    int twelf = 12.5;
    typedef reference_wrapper<int> rint;

    MyClass<float, rint> mc;
    vector<rint>& i = mc.access<rint>();

    i.push_back(twelf);

    mc.access<float>().push_back(10.5);

    cout << "Test:\n";
    cout << "floats: " << mc.access<float>()[0] << endl;
    cout << "ints: " << mc.access<rint>()[0] << endl;
    //mc.access<double>();

    return 0;
}

If you use any type that is not in the list of types you passed to specialize MyClass (see this commented-out access for double), you'll get a compile error, not too readable, but gcc at least points the correct place that has caused the problem and at least such an error message suggests the correct cause of the problem - here, for example, if you tried to do mc.access<double>():

 error: ‘value’ is not a member of ‘MyClass<float, int>::VectorOfType<2, double>’
Mishear answered 15/1, 2015 at 11:38 Comment(8)
Works good so far, thanks! What, however, if I wanted a vector<std::reference_wrapper<T>> instead? I get the following errors: error C2504: 'std::tuple_element<0,std::tuple<>>' : base class undefined see reference to class template instantiation 'std::tuple_element<1,std::tuple<std::vector<std::reference_wrapper<std::reference_wrapper<unsigned int>>,std::allocator<std::reference_wrapper<std::reference_wrapper<unsigned int>>>>>>' being compiled ...Limoges
Did you #include <functional> and compile in C++11 mode? I changed my testcase a bit, used reference_wrapper<int>, pushed an int variable and everything still normally works. I'll put the whole testcase function so that you can see it.Mishear
Thanks a lot, it works! What triggered the compiler errors was that I had some code, as described above by Richard Hodges, which tried to automatically insert a vector of boost::any's into the member vectors of the corresponding types.Limoges
It must work, I spent quite a big effort to define it - but it was worth it, even as an excercise for myself :)Mishear
This is a fantastic piece of template wizardry, and I've learnt some interesting things from it. Thank you! I have one concern – it looks like it creates a set of templated functions, one for each template type. The call stack of the generated function will be N levels deep for the Nth type, as MatchingField::get() calls itself recursively. My question is: Is the compiler likely to be able to squash this down into one function, or even to a single inlined pointer dereference? Since everything is static at runtime, it seems it should be possible, but I'm not 100% sure.Linders
You can even go further and provide in this MyClass some method forwarders to these vectors, which will select correct vector basing on type of the value you passed. For example, you can add a push_back method to this class, which will extract the correct vector by the type of data passed to MyClass::push_back and then forward it o this vector's push_back method.Mishear
You don't need the MatchingField machinery, you can just use the type overload of std::get, looking for vector<T>Henbane
This MatchingField machinery's purpose is exactly this "looking for". So I don't understand how std::get could be of any help here.Mishear
S
17

An alternate solution that doesn't use tuples is to use CRTP to create a class hierarchy where each base class is a specialization for one of the types:

#include <iostream>
#include <string>

template<class L, class... R> class My_class;

template<class L>
class My_class<L>
{
public:

protected:
  L get()
  {
    return val;
  }

  void set(const L new_val)
  {
    val = new_val;
  }

private:
  L val;
};

template<class L, class... R>
class My_class : public My_class<L>, public My_class<R...>
{
public:
  template<class T>
  T Get()
  {
    return this->My_class<T>::get();
  }

  template<class T>
  void Set(const T new_val)
  {
    this->My_class<T>::set(new_val);
  }
};

int main(int, char**)
{
  My_class<int, double, std::string> c;
  c.Set<int>(4);
  c.Set<double>(12.5);
  c.Set<std::string>("Hello World");

  std::cout << "int: " << c.Get<int>() << "\n";
  std::cout << "double: " << c.Get<double>() << "\n";
  std::cout << "string: " << c.Get<std::string>() << std::endl;

  return 0;
}
Simmonds answered 2/11, 2018 at 4:54 Comment(4)
I think that this requires that each element have a unique type. Is there a way around that?Swatch
An advantage to this solution as compared to the tuple solution is that this one will use less memory when the data types aren't aligned. For example, a tuple of uint32 and uint16 is 8 bytes in size, not 6.Swatch
@Eyal, this solution should also result in a size of 8 bytes for the example you give. The uint32 will cause the minimum alignment of the class to be 4 bytes, so the added 2 bytes of uint16 from second parent class will need to be followed by 2 padding bytes in order to align the class correctly. Even if you swap the parent classes, you should still end up with 8 bytes, as the uint16 will still need 2 padding bytes after it in order for the uint32 that comes after it to be aligned correctly. So either way, you should end up with 8 bytes unless you use some non-standard packing pragma.Dmz
Overall, the tuple-based solution seems to be the more efficient one, size-wise. For example, if you have a uint32, uint16, uint8, and uint64, in that order, the tuple-based solution results in a class size of 16 bytes, while the inheritance-based (this) solution results in a class size of 32 bytes using the latest versions of both clang and gcc (haven't tested with msvc). You can see it in action here: godbolt.org/z/vfd6zqzdz . If you order the types from large to small, you get the same efficiency in both cases, but that can be difficult with more complex types.Dmz
S
5

One way to do such a thing, as mentioned in πάντα-ῥεῖ's comment is to use a tuple. What he didn't explain (probably to save you from yourself) is how that might look.

Here is an example:

using namespace std;

// define the abomination    
template<typename...Types>
struct thing
{
    thing(std::vector<Types>... args)
    : _x { std::move(args)... }
    {}

    void print()
    {
        do_print_vectors(std::index_sequence_for<Types...>());
    }

private:
    template<std::size_t... Is>
    void do_print_vectors(std::index_sequence<Is...>)
    {
        using swallow = int[];
        (void)swallow{0, (print_one(std::get<Is>(_x)), 0)...};
    }

    template<class Vector>
    void print_one(const Vector& v)
    {
        copy(begin(v), end(v), ostream_iterator<typename Vector::value_type>(cout, ","));
        cout << endl;
    }

private:
    tuple<std::vector<Types>...> _x;
};


// test it
BOOST_AUTO_TEST_CASE(play_tuples)
{
    thing<int, double, string> t {
        { 1, 2, 3, },
        { 1.1, 2.2, 3.3 },
        { "one"s, "two"s, "three"s }
    };

    t.print();
}

expected output:

1,2,3,
1.1,2.2,3.3,
one,two,three,
Smash answered 14/1, 2015 at 11:53 Comment(6)
ah, I missed the extra constructor taking vector arguments. I was messing around with initializer lists of the std::tuple<std::vector>> and ran into the explicit nature of these constructors.Joacimah
It's a fun exercise but if I saw this in our production code I'd be looking to fire someone :-)Smash
this type of exercise can have benefits, e.g. in data oriented design you are often looking at std::tuple<std::vector> instead of std::vector<std::tuple> because of better caching etc. It can be convenient however to provide the interface of the latter on top of the data layout of the former. If you want to avoid hand-coding such a transformation, some variadic tuple magic can come in handy!Joacimah
@RichardHodges Good to know you aren't my boss ;)! I've got something like this in production. Unfortunately it's one of my most downvoted questions (where I think the DV's were just given as conciously chosen revenge).Saturant
Thanks! The code depends on c++14 features not available to my VS2013 though. Is something similar feasible with c++11 only?Limoges
@Limoges writing your own index_sequence_for is easy, and should be easy to find using google.Excitation
C
1

There is a proposal to allow this kind of expansion, with the intuitive syntax: P1858R1 Generalized pack declaration and usage. You can also initialize the members and access them by index. You can even support structured bindings by writing using... tuple_element = /*...*/:

template <typename... Ts>
class MyClass {
    std::vector<Ts>... elems;
public:
    using... tuple_element = std::vector<Ts>;

    MyClass() = default;
    explicit MyClass(std::vector<Ts>... args) noexcept
        : elems(std::move(args))...
    {
    }

    template <std::size_t I>
        requires I < sizeof...(Ts)
    auto& get() noexcept
    {
        return elems...[I];
    }

    template <std::size_t I>
        requires I < sizeof...(Ts)
    const auto& get() const
    {
        return elems...[I];
    }

    // ...
};

Then the class can be used like this:

using Vecs = MyClass<int, double>;

Vecs vecs{};
vecs.[0].resize(3, 42);

std::array<double, 4> arr{1.0, 2.0, 4.0, 8.0};
vecs.[1] = {arr.[:]};

// print the elements
// note the use of vecs.[:] and Vecs::[:]
(std::copy(vecs.[:].begin(), vecs.[:].end(),
           std::ostream_iterator<Vecs::[:]>{std::cout, ' '},
 std::cout << '\n'), ...);
Chamberlain answered 25/2, 2020 at 3:40 Comment(2)
Dear @L. F. Could you explain more you solution? I have tried this but the program fails with a lot of errors: see here: godbolt.org/z/6h6q8h574Giga
@EugeneW. This was a proposal that hasn’t been accepted to the best of my knowledge, so I don’t think it works in any current compiler. Maybe check back in a few years :)Chamberlain
E
0

Here is a less than perfectly efficient implementation using boost::variant:

template<typename ... Ts>
using variant_vector = boost::variant< std::vector<Ts>... >;

template<typename ...Ts>
struct MyClass {
  using var_vec = variant_vector<Ts...>;
  std::array<var_vec, sizeof...(Ts)> vecs;
};

we create a variant-vector that can hold one of a list of types in it. You have to use boost::variant to get at the contents (which means knowing the type of the contents, or writing a visitor).

We then store an array of these variant vectors, one per type.

Now, if your class only ever holds one type of data, you can do away with the array, and just have one member of type var_vec.

I cannot see why you'd want one vector of each type. I could see wanting a vector where each element is one of any type. That would be a vector<variant<Ts...>>, as opposed to the above variant<vector<Ts>...>.

variant<Ts...> is the boost union-with-type. any is the boost smart-void*. optional is the boost there-or-not.

template<class...Ts>
boost::optional<boost::variant<Ts...>> to_variant( boost::any );

may be a useful function, that takes an any and tries to convert it to any of the Ts... types in the variant, and returns it if it succeeds (and returns an empty optional if not).

Excitation answered 14/1, 2015 at 14:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.