【问题标题】:Store arbitrary elements in contiguous memory将任意元素存储在连续内存中
【发布时间】:2016-10-02 20:15:22
【问题描述】:

我正在尝试创建一个数据结构,它将在连续内存中保存 N 种不同类型。所以在编译时我可以说我想存储 3 种不同类型的 4 个元素,在内存中它看起来像 111122223333。

我一直在使用可变参数模板方法,我认为它会做我想要的,但是我不确定如何在 add 方法中将元素添加到每个数组中。

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

即使我让它工作,内存布局是否正确?有更好的方法吗?

【问题讨论】:

  • 根据 Ildjarn 指出的可能问题修改了我的答案;简而言之:如果Args... 不是 POD 类型,则从不使用我的解决方案
  • 关于我为什么提出这个问题以及为什么我接受答案的一些背景信息,即使有警告也是如此。这最终将用于将缓冲区传递给 OpenGL,上面的代码只是一个示例。在实际代码中,我将检查 sizeof 类型是否可以被我要传递给 OpenGL 的类型(当前为浮点数)整除,这样可以缓解对齐问题。至于使用std::string 之类的东西作为类型的问题,这是有效的,我相信如果我在std::is_trivially_copyable 上断言,它应该是安全的。在实践中,不同的类型将是不同大小的向量。

标签: c++ c++11 templates variadic-templates memory-alignment


【解决方案1】:

我不认为这是一个好主意,但是……我只是为了好玩而展示它

使用std::vector&lt;char&gt;(以及C++11添加的方法data()授予的对以下内存的访问)和旧的memcpy(),我想你可以简单地按照以下方式进行

#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 ---:添加了一个方法,toCout() 打印(到std::cout)所有存储的值;只是建议如何使用这些值。

--- 编辑 2 ---:正如 ildjarn 所指出的(谢谢!),如果在 Args... 类型中是一些非 POD(普通旧数据)类型,则此解决方案非常危险。

this page 中解释的很好。

我转录相关部分

无法使用 memcpy 安全复制的类型的示例是 标准::字符串。这通常使用引用计数来实现 共享指针,在这种情况下,它将有一个复制构造函数 导致计数器递增。如果使用 memcpy 制作了副本 那么复制构造函数将不会被调用并且计数器将是 留下比应有的值低一的值。这很可能 导致包含 字符数据。

--- 编辑 3 ---

正如 ildjarn 所指出的(再次感谢!),使用此解决方案离开 data() 成员非常危险。

如果有人使用这样返回的指针

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

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

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

在某些架构中,可能会导致访问未对齐地址中的 float *,从而导致程序终止

data() 返回的指针中恢复值的正确(安全)方法是toCoutHelper() 中使用的方法,使用`std::memcpy()

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

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

   float  f; 

   std::memcpy( & f, pv + pos, sizeof(f) );

【讨论】:

  • 甜蜜!我很确定这对我有用。
  • @ildjarn - 我也有同样的恐惧,因此,我不鼓励这种(只是为了好玩)解决方案。但是……您认为哪条指令是危险的?为什么?
  • @ildjarn - 不确定...你认为这很危险吗std::memcpy(buffer.data() + pos, &amp; v0, sizeof(T));?但是buffer.data()(如果我没记错的话)是指向char的指针; memcpy() 也可以与 char 指针存在内存对齐问题?
  • @ildjarn - 关于非平凡可复制类型的要点;我将修改我的答案以计算这一点;谢谢。但是(如果我没记错的话)std::string 不是对齐问题)(Tv0T 对齐,我想)而是内部引用计数器问题。
  • @ildjarn - 我明白了......好吧,你关于Batch::data()(或者更好的是:错误使用返回的指针)的危险性是一个很好的观点;我很清楚,但并非所有读者都清楚。我将尝试在“编辑 3”中进行解释(希望我的英语可以理解)。无论如何,您如何看待我们的 cmets 对这个答案的看法?不会有让偶尔的读者感到困惑的风险吗?如果我们删除它们会更好?
【解决方案2】:

有两种词汇类型可以帮助您。
std::variantstd::any

std::variant 更适合您的预期用途。

不要像这样创建自己的类型:

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

考虑使用:

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

然后就可以正常添加元素了:

vec.emplace_back(1);    //int
vec.emplace_back(1.0f); //float
vec.emplace_back(1.0);  //double

【讨论】:

  • 谢谢,虽然这可能是浪费内存的问题。例如,如果我有 std::vector<:variant mystruct>>,其中 MyStruct 包含 5 个成员,则 float 的每个元素将占用 5 倍的内存,对吧?
  • @dempzorz 是的。这是因为只有一个对齐和大小来处理可以简化调整内存大小。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-06-02
  • 2023-03-22
  • 2012-02-29
  • 2020-04-20
  • 2014-08-04
  • 1970-01-01
  • 2016-04-17
相关资源
最近更新 更多