Вопрос

I am having trouble going the second step or level in templating my code. I have stripped the code to its bare essentials for readability.

I have looked through a lot of templates questions, but I was not really able to solve my exact issue.

I currently have a class RIVRecord, which I templated like this

template <class T>
class RIVRecord
{

private:
    std::vector<T> values;
public:

    std::string name;

    RIVRecord(std::string _name, std::vector<T> _values) { name = _name; values = _values;  };
    ~RIVRecord(void) {  };

    size_t size() {
        return values.size();
    }

    T* Value(int index) {
        return &values[index];
    }
}

Easy enough. The T types are usually primitive types such as floats and integers. Then I want to put these RIVRecords in a DataSet class. Here is where I am having more difficulty. Untemplated it would be something like this:

class RIVDataSet
{
private :
    //How to template this??
    vector<RIVRecord<float>> float_records;
    vector<RIVRecord<int>> int_records;

public:
    RIVDataSet(void);
    ~RIVDataSet(void);
    //And this
    void AddRecord(RIVRecord<float> record) {
        //How would this work?    
    }
    //And this?
    RIVRecord<float> GetFloatRecord();
};

I come from a Java background, so there I could use the vector<?> and do type checking whenever I ask a RIVRecord. But this does not seem possible in C++. I tried using variadic templates but am unsure how to construct the vector using all types in the template :

template <class... Ts>
class RIVDataSet
{
private :        
    //For each T in Ts
    vector<RIVRecord<T>> records;

public:
    RIVDataSet(void);
    ~RIVDataSet(void);
    //For each T in Ts
    void AddRecord(RIVRecord<T> record) {
        //How would this work?    
    }
    //For each T in Ts, get the record by index.
    RIVRecord<T> GetRecord(int index);
};

I already saw that this sort of iteration in C++ templates is not possible, but it is just to clarify what I would want.

Any help is very welcome, thank you.

EDIT:

There is no restriction on the number of types (floats, ints,...) for T

Also, GetRecord works by having an index, but I don't really care about it that much, as long as I can iterate over the records and get the right type.

Это было полезно?

Решение

Solving this via variadic templates is not very complicated, but requires some additional support types. Let us begin, by looking at the result:

template <typename... V>
class many_vectors
{
    static_assert(are_all_different<V...>::value, "All types must be different!");

    std::tuple<std::vector<V>...> _data;

public:
    template<typename T>
    std::vector<T>& data()
    { return std::get<index_of<T, V...>::value>(_data); }

    template<typename T>
    std::vector<T> const& data() const
    { return std::get<index_of<T, V...>::value>(_data); }

    template<typename T>
    void push_back(T&& arg)
    { data<typename std::remove_reference<T>::type>().push_back(std::forward<T>(arg)); }

    template<typename T, typename... W>
    void emplace_back(W&&... args)
    { data<T>().emplace_back(std::forward<W>(args)...); }
};

The static_assert defines a very important requirement: Since we are differentiating on types, we must ensure that all types are different. The _data member is a std::tuple of the vectors for the different types, and corresponds directly to your float_records and int_records members.

As an example of providing a member function that refers to one of the vectors by their type the data function exposes the individual vectors. It uses a helper template to figure out which element of the tuple corresponds to your type and gets the result.

The push_back function of the vectors is also exposed to show how to use that to provide functions on these. Here std::forward is used to implement perfect forwarding on the argument to provide optimal performance. However, using rvalue references in combination with templates parameter deduction can lead to slightly unexpected results. Therefore, any reference on the T parameter is removed, so this push_back will not work for a many_vectors containing reference types. This could be fixed by instead providing two overloads push_back<T>(T&) and push_back<T>(T const&).

Finally, the emplace_back exposes a function that cannot rely on template parameter argument deduction to figure out which vector it is supposed to utilize. By keeping the T template parameter first, we allow a usage scenario in which only T is explicitly specified.

Using this, you should be ably to implement arbitrary additional members with similar funcitonality (e.g. begin<T> and end<T>).

Helpers

The most important helper is very simple:

template<typename T, typename U, typename... V>
struct index_of : std::integral_constant<size_t, 1 + index_of<T, V...>::value>
{ };

template<typename T, typename... V>
struct index_of<T, T, V...> : std::integral_constant<size_t, 0>
{ };

This will fail with a fairly ugly error message, if the first argument is not one of the following at all, so you may wish to improve on that.

The other helper is not much more complicated:

template<typename T, typename... V>
struct is_different_than_all : std::integral_constant<bool, true>
{ };

template<typename T, typename U, typename... V>
struct is_different_than_all<T, U, V...>
    : std::integral_constant<bool, !std::is_same<T, U>::value && is_different_than_all<T, V...>::value>
{ };

template<typename... V>
struct are_all_different : std::integral_constant<bool, true>
{ };

template<typename T, typename... V>
struct are_all_different<T, V...>
    : std::integral_constant<bool, is_different_than_all<T, V...>::value && are_all_different<V...>::value>
{ };

Usage

Yes, usage is as simple as you might hope:

v.push_back(int(3));
v.push_back<float>(4);
v.push_back<float>(5);
v.push_back(std::make_pair('a', 'b'));
v.emplace_back<std::pair<char, char>>('c', 'd');

std::cout << "ints:\n";
for(auto i : v.data<int>()) std::cout << i << "\n";

std::cout << "\n" "floats:\n";
for(auto i : v.data<float>()) std::cout << i << "\n";

std::cout << "\n" "char pairs:\n";
for(auto i : v.data<std::pair<char, char>>()) std::cout << i.first << i.second << "\n";

With the expected result:

ints:
3

floats:
4
5

char pairs:
ab
cd

Другие советы

You can use a technique called type erasure, you'll have to include another level of indirection however. Some general feedback:

RIVRecord(std::string _name, std::vector<T> _values)

Is better as:

RIVRecord(const std::string& _name, const std::vector<T>& _values)

In order to avoid unnecessary copies, overall the rule of thumb is to accept arguments as const& for most things which aren't a primitive.

T* Value(int index) { return &values[index]; }

Is dangerous, if the size() goes beyond capacity() of your vector< T > it will reallocate and invalidate all your pointers. A better interface in my opinion would be to have a T GetValue< T >() & void SetValue< T >( T a_Value ).

On to type erasure, this is how RIVDataSet could look, I'm using a library called Loki here, if you don't want to use Loki I'll give you some pointers afterwards.

class RIVDataSet
{
private :
    //How to template this??
    struct HolderBase
    {
        virtual ~HolderBase() {}
    };
    template< typename T >
    struct HolderImpl : HolderBase
    {
        // Use pointer to guarantee validity of returned record
        std::vector< RIVRecord< T >* > m_Record;
    };
    typedef Loki::AssocVector< Loki::TypeInfo, HolderBase* > HolderMap;
    HolderMap m_Records;
public:
    ~RIVDataSet() 
    { 
        for( HolderMap::iterator itrCur = m_Records.begin(); itrCur != m_Records.end(); ++itrCur ) delete itrCur->second; 
    }
    //And this
    template< typename T >
    void AddRecord(const RIVRecord< T >& record ) 
    {
        HolderMap::iterator itrFnd = m_Records.find( typeid( T ) );
        if( itrFnd == m_Records.end() )
            itrFnd = m_Records.insert( std::make_pair( Loki::TypeInfo( typeid( T ) ), new HolderImpl< T >() ) ).first;
        static_cast< HolderImpl< T >* >( itrFnd->second )->m_Record.push_back( new RIVRecord< T >( record ) );
    }
    template< typename T >
    RIVRecord< T >* GetRecord()
    {
        HolderMap::iterator itrFnd = m_Records.find( typeid( T ) );
        assert( itrFnd != m_Records.end() );
        return itrFnd == m_Records.end() ? 0 : static_cast< HolderImpl< T >* >( itrFnd->second )->m_Record.front();
    }
};

Loki::AssocVector can be substituted for std::map, you do however need Loki::TypeInfo, which is just a wrapper for std::type_info. It's fairly easy to implement one your self if you take a look at the code for it in Loki.

One horrible idea if you really must do it as general is using the "type erasure idiom". It goes something like this (haven't compiled that though but I think it will, and can be further improved by type traits that would link RIVRecordsIndex::Float to the type float and prevent error)

class BaseRIVRecord                                                                                                                                          
{                                                                                                                                                            
};                                                                                                                                                           

template <class T>                                                                                                                                           
class RIVRecord : public BaseRIVRecord                                                                                                                       
{                                                                                                                                                            
};                                                                                                                                                           

enum class RIVRecordsIndex                                                                                                                                   
{                                                                                                                                                            
   Float, Int                                                                                                                                                   
};                                                                                                                                                           

class RIVDataSet                                                                                                                                             
{                                                                                                                                                            
public:                                                                                                                                                         
    template<RIVRecordsIndex I, typename T>                                                                                                                                         
    void addRecord()                                                                                                                                
    {                                                                                                                                                            
         allmightyRecords.resize(I+1);                                                                                                                            
         allmightyRecords[I].push_back(new RIVRecord<T>());                                                                                                       
    }                                                                                                                                                            

    template<RIVRecordsIndex I, typename T>                                                                                                                                         
    RIVRecord<T>* get(unsigned int index)                                                                               
    {                                                                                                                                                            
         return static_cast<RIVRecord<T>*>(allmighyRecords[I][index]);                                                                               
    }
private:                                                                                                                                                            
    std::vector<std::vector<BaseRIVRecord*>> allmightyRecords;                                                                                                 
};                                                                                                                                                           

int main()
{
     RIVDataSet set;
     set.addRecord<RIVRecordsIndex::Float, float>();
     set.addRecord<RIVRecordsIndex::Float, float>();
     set.addRecord<RIVRecordsIndex::Int, int>();

     RIVRecord<int> r = set.get<RIVRecordsIndex::Int, int>(0);
}

If you decide to do this stuff make sure you do not slice the inherited type (i.e. use vector of pointers). Do use some kind of type traits to prevent error calls like set.get. Again I have no time to actually compile that, it is just an idea thrown to further develop.

You can't use variadic templates to create multiple members of the same name but different type. In fact, you can never have two members with the same name. However, you can use multiple inheritance, and put the member in your base classes using variadic base classes. You can then use a member template in your derived class to resolve the ambiguity.

The example below also uses perfect forwarding to make sure that if a temporary is passed to add(), its resources can be "stolen". You can read more about that here.

Here is the example:

#include <vector>
#include <utility>

// This templated base class holds the records for each type.
template <typename T>
class Base {
    public:
        // "T &&v" is a universal reference for perfect forwarding.
        void add(T &&v) { records.push_back(std::forward<T>(v)); }
    private:
        std::vector<T> records;
};

// This inherits from Base<int>, Base<double>, for example, if you instantiate
// DataSet<int, double>.
template <typename... Ts>
class DataSet : public Base<Ts>... {
    public:
        // The purpose of this member template is to resolve ambiguity by specifying
        // which base class's add() function we want to call.  "U &&u" is a
        // universal reference for perfect forwarding.
        template <typename U>
        void add(U &&u) {
            Base<U>::add(std::forward<U>(u));
        }
};

int main() {
    DataSet<int, double> ds;
    ds.add(1);
    ds.add(3.14);
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top