2

I am trying to create a data structure, where it will hold N number of different types in contiguous memory. So at compile time I can say I want to store 4 elements of 3 different types, and in memory it will look like 111122223333.

I've been going with a variadic template approach, which I think will do what I want, however I am not sure how to add the elements to each array in the add method.

template<std::size_t N, typename... Args>
class Batch
{
    private:
        std::tuple<std::array<Args, N>...> data_;
        size_t currentPos_;

    public:
        template<typename T>
        void addToArray(std::array<T, N>& array, const T& value)
        {
            array[currentPos_] = value;
        }

        void add(const Args&... values)
        {
            //????
            addToArray(/*array, value*/);

            currentPos_++;
        }

        const void* data()
        {
            &return data_;
        }
};


int main()
{
    Batched<3, float, double, int> b;

    b.add(1.0f, 1.0, 1);
    b.add(2.0f, 2.0, 2);
    b.add(3.0f, 3.0, 3);
    b.add(4.0f, 4.0, 4);
    return 0;
}

Even if I get this to work, will the memory layout be correct? Is there a better approach?

max66
  • 65,235
  • 10
  • 71
  • 111
dempzorz
  • 1,019
  • 13
  • 28
  • modified my answer according to a possible issue pointed by Ildjarn; in short: **never** use my solution if in `Args...` are not-POD types – max66 Oct 03 '16 at 13:40
  • 1
    A bit of context on why I asked the question and why I accepted the answer, even with the warnings. This is ultimately going to be used to pass a buffer to OpenGL, and the code above is just an example. In the real code I will check that the `sizeof` the type is divisible by the type I want to pass to OpenGL (currently float), so that should alleviate alignment concerns. As to the issue with using things like `std::string` as a type, that is valid, and I believe if I assert on `std::is_trivially_copyable`, it should be safe. In practice, the different types will be different sized Vectors. – dempzorz Oct 03 '16 at 17:51

2 Answers2

3

I don't think it's a good idea but... I show it just for fun

Using a std::vector<char> (and the access to the following memory granted by the C++11 added method data()) and the good-old memcpy(), I suppose You can simply do as follow

#include <vector>
#include <cstring>
#include <iostream>

template <typename... Args>
class Batch
 {
   private:
      std::vector<char> buffer;

   public:

      void addHelper ()
       { }

      template <typename T, typename ... Ts>
      void addHelper (T const & v0, Ts ... vs)
       { 
         auto  pos = buffer.size();

         buffer.resize(pos + sizeof(T));

         std::memcpy(buffer.data() + pos, & v0, sizeof(T));

         addHelper(vs...);
       }

      void add (const Args&... values)
       { addHelper(values...); }

      const void * data()
       { return buffer.data(); }

      void toCout ()
       { toCoutHelper<Args...>(0U, buffer.size()); }

      template <typename T, typename ... Ts>
      typename std::enable_if<(0U < sizeof...(Ts)), void>::type
         toCoutHelper (std::size_t  pos, std::size_t  size)
       {
         if ( pos < size )
          {
            T val;

            std::memcpy( & val, buffer.data() + pos, sizeof(T) );

            std::cout << " - " << val << std::endl;

            toCoutHelper<Ts...>(pos+sizeof(T), size);
          }
       }

      template <typename T, typename ... Ts>
      typename std::enable_if<0U == sizeof...(Ts), void>::type
         toCoutHelper (std::size_t  pos, std::size_t  size)
       {
         if ( pos < size )
          {
            T val;

            std::memcpy( & val, buffer.data() + pos, sizeof(T) );

            std::cout << " - " << val << std::endl;

            toCoutHelper<Args...>(pos+sizeof(T), size);
          }
       }

 };


int main()
 {
   Batch<float, double, int> b;

   b.add(1.0f, 1.0, 1);
   b.add(2.0f, 2.0, 2);
   b.add(3.0f, 3.0, 3);
   b.add(4.0f, 4.0, 4);

   b.toCout();

   return 0;
 }

--- EDIT ---: added a method, toCout() that print (to std::cout) all the stored values; just to suggest how to use the values.

--- EDIT 2 ---: As pointed by ildjarn (thanks!) this solution is very dangerous if in the Args... types are some non POD (Plain Old Data) type.

It's explained well in this page.

I transcribe the relevant part

An example of a type that cannot be safely copied using memcpy is std::string. This is typically implemented using a reference-counted shared pointer, in which case it will have a copy constructor that causes the counter to be incremented. If a copy were made using memcpy then the copy constructor would not be called and the counter would be left with a value one lower than it should be. This would be likely to result in premature deallocation of the memory block that contains the character data.

--- EDIT 3 ---

As pointed by ildjarn (thanks again!) with this solution is very dangerous to leave the data() member.

If anyone use the pointer returned in this way

   char const * pv = (char const *)b.data();

   size_t  pos = { /* some value here */ };

   float  f { *(float*)(pv+pos) };  // <-- risk of unaligned access

could, in some architecture, cause an access to a float * in an unaligned address that can kill the program

The correct (and safe) way to recover values from the pointer returned by data() is the one used in toCoutHelper(), using `std::memcpy()

   char const * pv = (char const *)b.data();

   size_t  pos = { /* some value here */ };

   float  f; 

   std::memcpy( & f, pv + pos, sizeof(f) );
max66
  • 65,235
  • 10
  • 71
  • 111
  • Sweet! I'm pretty sure this is going to work for me. – dempzorz Oct 02 '16 at 22:07
  • @ildjarn - I have the same fear, and, for that reason, I discourage this (just for fun) solution. But... which exactly instruction do you think is dangerous? And why? – max66 Oct 03 '16 at 11:43
  • @ildjarn - not sure to understand... do you think that is dangerous `std::memcpy(buffer.data() + pos, & v0, sizeof(T));`? But `buffer.data()` (if I'm not wrong) is a pointer to `char`; `memcpy()` can have memory alligment issues with `char` pointers too? – max66 Oct 03 '16 at 12:10
  • @ildjarn - good point about the non-trivially-copyable types; I'll modify my answer to take in count this; thanks. But (if I'm not wrong) with a `std::string` isn't an alligment problem) (the `T` value `v0` is `T` alligned, I suppose) but a internal reference counter problem. – max66 Oct 03 '16 at 12:41
  • @ildjarn - also `int`? Also `float`? I dont' understand why. – max66 Oct 03 '16 at 12:44
  • @ildjarn - yes it's a good thing; can you show me some of this asnwers? – max66 Oct 03 '16 at 12:49
  • @ildjarn - the suggestion to post a question is a good one: [posted](http://stackoverflow.com/questions/39832733/plain-old-data-and-stdmemcpy-alignment-issues); I've expressed it correctly? Is conforming to your objection? – max66 Oct 03 '16 at 13:29
  • @ildjarn - In my question I've received only a couple of answers which states that my mode of using `std::memcpy` isn't dangerous; please, could you add an answer explaining where the danger is? – max66 Oct 04 '16 at 19:29
  • On my initial reading it was not clear to me that `toCoutHelper` is the _only_ place that `buffer.data()` is actively read from, and that code _is_ indeed safe. `Batch::data()` is very dangerous – the only way to use it is by casting it to some other type, there's no way to constrain that to `char const*`, and any _other_ type has alignment problems) – and I object to its presence, but there's nothing wrong with the code as shown. Apologies for critiquing without reading more thoroughly! – ildjarn Oct 05 '16 at 00:02
  • 1
    @ildjarn - I see... Well, your point about the dangerousness of `Batch::data()` (or better: of a wrong use of the returned pointer) is a good one; it's clear to me but it isn't necessarily clear to all readers. I'll try to explain in an "Edit 3" (hoping my English will be understandable). Anyway, what do you think about our comments to this answer? There isn't the risk that they can confuse an occasional reader? It's better if we delete they? – max66 Oct 05 '16 at 17:47
2

There are two vocabulary types that can help you.
std::variant and std::any.

std::variant is closer fit based on your intended use.

Instead of creating your own type like this:

Batched<3, float, double, int> b;

consider using:

std::vector<std::variant<float, double, int>> vec;

You can then add elements normally:

vec.emplace_back(1);    //int
vec.emplace_back(1.0f); //float
vec.emplace_back(1.0);  //double
Trevor Hickey
  • 36,288
  • 32
  • 162
  • 271
  • Thanks, although this could be an issue with wasting memory. For instance, if I had std::vector>, where MyStruct contained 5 members, each element for float would take 5 times the memory, right? – dempzorz Oct 02 '16 at 20:33
  • @dempzorz Yes. This is because having one single alignment and size to deal with simplifies resizing memory. – user2296177 Oct 03 '16 at 15:12