【问题标题】:Deny std::vector from deleting its data拒绝 std::vector 删除其数据
【发布时间】:2016-08-07 05:16:37
【问题描述】:

我有以下情况:

T* get_somthing(){
    std::vector<T> vec; //T is trivally-copyable
    //fill vec
    T* temp = new T[vec.size()];
    memcpy(temp, vec.data(), vec.size() * sizeof(T));
    return temp;
}

我想通过像这样直接返回std::vector::data来摆脱复制过程:

T* get_somthing(){
    std::vector<T> vec; //T is trivally-copyable
    //fill vec
    return temp.data();
}

但是,这是错误的,因为当调用 vec 析构函数时,数据将被删除。

那么,如何防止 vec 删除其数据?换句话说,我想要某种从 std::vector 到 C++ 原始动态数组的移动习惯。

附:改变设计不是一种选择。使用std::vector 是强制性的。将pointer 返回到array 也是强制性的。因为它是两个模块之间的包装器。一个需要向量,另一个需要指针。

【问题讨论】:

  • 有什么理由不返回向量?
  • 我不明白为什么你必须使用向量,它在函数实现中,用户不应该知道你在做什么。 :)
  • 顺便说一句,您可能想看看 N4359:open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4359.pdf 建议用于 C++17
  • 我想这可以使用自定义分配器,但我不确定您的向量是否可以使用一个。
  • @TomaszLewowski 这在 LEWG 中被击落。

标签: c++ c++11 vector


【解决方案1】:

附:改变设计不是一种选择。使用 std::vector 是强制性的。返回指向数组的指针也是强制性的。

更改设计是您的最佳选择。我建议重新考虑这种立场。

(目前)没有办法“窃取”向量的缓冲区,因此鉴于问题中所述的(愚蠢††)限制,复制是要走的路。

† Tomasz Lewowski 链接了一个提案,如果它被包含在未来的标准中,它将改变这一点:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4359.pdf(编辑:正如所指出的,它被 c++17 拒绝)

†† 在具体要求证明合理之前是愚蠢的。


它是两个模块之间的包装器。一个需要向量,另一个需要指针。

可能需要指针的另一个接口将缓冲区的销毁委托给调用者,可能使用某种类型的回调,例如void delete_somthing(T*)。在我看来,在不归还所有权的情况下取得所有权是非常糟糕的设计。

如果您确实可以控制销毁,您可以将向量存储在映射中,并在将指针传递给销毁时擦除该向量:

std::unordered_map<T*, std::vector<T>> storage;

T* get_somthing(){
    std::vector<T> vec; //T is trivally-copyable
    //fill vec
    T* ptr = vec.data();
    storage[ptr] = std::move(vec);
    return ptr;
}

void delete_somthing(T* ptr){
    storage.erase(ptr);
}

【讨论】:

  • 我可以就vector::release() 提案的拒绝发表意见。一般来说(使用现代自定义分配器),正确处理向量资源(正确实现~vector())不是初学者代码。仅仅在那个指针上调用delete[] 是不正确的,即使你知道vector 正在使用std::allocator。当您正确构建释放vector::release() 返回的指针所需的所有机制时,您已经重新发明了相当一部分vector。这将比unique_ptr&lt;T[]&gt;::release() 更难使用一个数量级。
  • @HowardHinnant “即使您知道该向量正在使用 std::allocator,仅在该指针上调用 delete[] 也是不正确的” 这对我来说似乎很奇怪。您能否提供详细说明的指针?
  • @NoSenseEtAl 如果向量释放了指向您的指针,则该指针将指向内存缓冲区,其中第一部分保存构造元素,后一部分保存原始内存。第一部分中关于如何销毁元素的指令由向量的分配器保存,一旦缓冲区不再保存任何构造的元素,解除分配缓冲区的指令也是如此。构造元素的数量由分配器保存,分配的缓冲区的大小也是如此。
  • @HowardHinnant 啊,我在考虑 POD 类型(我想有更多花哨的术语,比如 is_trivial)......无论如何,如果你想提出我的建议,添加需要向量 T 的版本成为 POD 那就太好了。 ;) 开个玩笑,我觉得标准应该说 vector 如果底层分配器使用 malloc 应该提供释放和采用方法(因此用户可以在收到的缓冲区上调用 free )。但我猜 WG21 会讨厌向“普通”用户公开复杂的方法......而且 AFAIK 没有先例提供容器提供方法或不基于 T. :/
【解决方案2】:

在 C++11 中,没有从向量中释放缓冲区的选项。

向 C++17 提出了对标准的这种扩展:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4359.pdf 但是,作为 T.C.指出,被拒绝:https://issues.isocpp.org/show_bug.cgi?id=81

因此,在标准方面没有运气。 还发布了一个已经回答并解释了相同问题的相关问题:Destroy std::vector without releasing memory

如果你能弄乱这些库中的任何一个,你可以尝试自定义分配器或其他奇怪的东西(比如将自己绑定到库的内部实现并弄乱私有向量数据),但实际上,不要。

【讨论】:

    【解决方案3】:

    以下是如何使用自定义分配器的示例代码。这假设您可以使 vector 实际使用自定义分配器。分配器也使用静态变量来控制内部缓冲区的破坏。我已经在 VS2015 下进行了检查,它的实现调用 deallocate in ~vector 仅用于内部缓冲区 - 即它不使用此分配器管理任何其他分配。

    这是一个 hack - 我不确定使用它可能会产生什么后果。它肯定不是线程安全的(但可以在将 allow_dealloc 线程设为本地后轻松修复)。:

    http://coliru.stacked-crooked.com/a/5d969a6934d88064

    #include <limits>
    #include <vector>
    #include <iostream>
    
    template <class T>
    class my_alloc {
      std::allocator<T> alloc;
    public:
      static bool allow_dealloc;
    
      typedef T        value_type;
      typedef T*       pointer;
      typedef const T* const_pointer;
      typedef T&       reference;
      typedef const T& const_reference;
      typedef std::size_t    size_type;
      typedef std::ptrdiff_t difference_type;
    
      pointer allocate(size_type num, const void* = 0) { return alloc.allocate(num);  }
      void deallocate(pointer p, size_type num) { 
          if (allow_dealloc) 
            alloc.deallocate(p, num*sizeof(T));  }
    
    
      // Squashed as less important
      template <class U> struct rebind { typedef my_alloc<U> other; };
      pointer address(reference value) const { return &value; }
      const_pointer address(const_reference value) const { return &value; }
      my_alloc() throw() { }
      my_alloc(const my_alloc&) throw() { }
      template <class U> my_alloc(const my_alloc<U>&) throw() { }
      ~my_alloc() throw() { }
      size_type max_size() const throw() { return (std::numeric_limits<size_t>::max)() / sizeof(T); }  
      void construct(pointer p, const T& value) { alloc.construct(p, value); }
      void destroy(pointer p) { p->~T(); }  
    };
    
    template <typename T>
    bool my_alloc<T>::allow_dealloc = true;
    
    int main()
    {
      int* data = 0;
      size_t size = 0;
      {
        my_alloc<int>::allow_dealloc = true;      
        std::vector<int, my_alloc<int>> vec= { 0, 1, 2, 3 };
        vec.push_back(4);
        vec.push_back(5);
        vec.push_back(6);
        my_alloc<int>::allow_dealloc = false;
        data = vec.data();
        size = vec.size();
      }
    
      for (size_t n = 0; n < size; ++n)
        std::cout << data[n] << "\n";
    
      my_alloc<int> alloc; 
      alloc.deallocate(data, size);
    }
    

    【讨论】:

      【解决方案4】:

      我不知道你是否会喜欢这个非常hacky的解决方案,我绝对不会在生产代码中使用它,但请考虑:

      #include <iostream>
      using namespace std;
      
      #include <vector>
      
      template<class T>
      T* get_somthing(){
          std::vector<T> vec = {1,2,3}; //T is trivally-copyable
      
          static std::vector<T> static_vector = std::move(vec);
      
          return static_vector.data();
      }  
      
      int main() {
          int * is = get_somthing<int>();
          std::cout << is[0] << " " << is[1] << " " << is[2];
          return 0;
      }
      

      所以,正如您在get_somthing 中看到的那样,我定义了一个与您需要的类型相同的静态向量,并在其上使用std::move,并返回它的data()。它实现了你想要的,但这是危险的代码,所以请使用老式的复制数据的方法,让我们等到 N4359 进入主流编译器。

      现场演示:http://ideone.com/3XaSME

      【讨论】:

      • N4359 不会进入主流编译器(被拒绝) - 请参阅我编辑的答案和问题下的 cmets
      • 静态局部变量不是在第一次调用函数时才初始化吗?也就是说,所有对get_somthing() 的调用都会产生相同的数据(即使vec 正在从某个地方获取可变数据)。
      • @PeterA.Schneider 你是对的,但我们可以编写如下代码:static std::vector&lt;T&gt; static_vector;static_vector = std::move(vec); 在这种情况下
      【解决方案5】:

      如果可以选择使用智能指针,我会推荐 std::shared_ptr 和别名:

      template<typename T>
      std::shared_ptr<T> get_somthing(){
          using Vector = std::vector<T>;
          using ReturnT = std::shared_ptr<T>;
          std::vector<T>* vec = new std::vector<T>;
          //fill vec
          std::shared_ptr<Vector> vectorPtr(vec); // (1)
          std::shared_ptr<T> aliasedPtr(vectorPtr, vec->data()); // (2)
          return aliasedPtr;
      }
      

      (1) 将创建一个指向要别名的向量的共享指针 (2) 创建一个共享指针,该指针将销毁别名 shared_ptr 而不是删除包含的数据

      【讨论】:

        【解决方案6】:

        编辑:这个想法行不通,因为没有办法阻止隐式调用基类的析构函数(感谢 molbdnilo)。如果我想,他们被称为是一件好事。


        我不完全确定这是否可行(并且好奇其他人怎么说),但是否有可能从 vector 继承并覆盖它的析构函数(什么都不做)?即使~vector() 不是虚拟的(标准中是否有虚拟或非虚拟的要求?)只要您明确使用您的类型,这应该可以工作。

        通过继承,您将保留所有好处,尤其是内存管理——除了最后一点(您不想要)。

        【讨论】:

        • 不能避免调用基类的析构函数。
        • @molbdnilo 哦——这很可悲:-(。
        【解决方案7】:

        到这里的方式不就是简单地动态分配向量吗?这就是传统上管理与范围无关的生命周期的资源的方式,我认为没有任何理由发明一些非凡的东西。

        当然,该向量应该在一段时间后销毁;这可能使得有必要将其地址存储在某处作为get_somthing() 的副作用,但即便如此,这种策略似乎也比其他任何想法都清晰。

        【讨论】:

        • 那会弄得一团糟。
        • @LightnessRacesinOrbit 没有其他解决方案对我来说更漂亮。 user207..的地图和这个想法一样:有副作用,以后再删除。另一个建议,自定义分配器,似乎太费力了,兼容性太少了。 shared_ptr 建议根本不符合要求(函数签名不同)。
        • 动态分配容器是不好的,动态分配任何东西然后返回指向它的原始指针的函数也是不好的。只需返回向量并让移动构造函数为您处理资源。如果你不能改变函数签名,那么就不要使用向量。
        【解决方案8】:

        你应该做的第一件事是起床,去找负责这个设计的人并(以专业的方式口头)打他/她的脸:这是一个一团糟

        然后,在 C++11 中有一种方法可以让 std::vector 具有自动存储持续时间并且不调用其析构函数:

        std::vector 放入union

        像这样:

        template<typename T>
        union Ugly {
          std::vector<T> vec;
          Ugly() {
            new (&vec) std::vector<T>(); // Construct
          }
          ~Ugly() {
           // Don't destruct
          }
        };
        
        T* get_something(){
          Ugly mess;
          //fill mess.vec
          return mess.vec.data();
        }
        

        我不能 100% 确定这是否仍然算作有效的 C++11,但它应该“工作”。现在对不起,我需要洗手以摆脱这段代码的哭泣羞耻感......

        哦,还有一件事:你打算如何释放std::vector 分配的内存?你知道,你不能(可靠地)使用成员函数data()返回的指针!

        【讨论】:

          【解决方案9】:

          好的,不要在家里尝试这个,这不太好。这更像是一个实验而不是一个实际的答案。

          (好吧,要求/设计也不好:“玩愚蠢的游戏,赢得愚蠢的奖品”)

          在您的 cpp 中:

          #define private public               // good luck for the code review
          #define protected public
          #include <vector>                    // (must be the first occurence in the TU)
          #undef private                       // do not abuse good things...
          #undef protected
          
          template<typename T>
          T* my_release(std::vector<T>& v){
              std::vector<T> x;                // x: the local vector with which we mess around
              std::swap(x, v);                 // the given vector is in an OK, empty state now.
              T* out = x._M_impl._M_start;     // first, get the pointer you want
          
              // x will be destructed at the next '}'. 
              // The dtr only use _M_start and _M_finish, make sure it won't do anything.
              x._M_impl._M_start = nullptr;    
              x._M_impl._M_finish = nullptr;
          
              // no need to say, the internal state of 'x' is bad, like really bad...
              // also we loose the capacity information, the actual allocator... 
              // -> good luck with memory leaks...
          
              return out;
          }
          
          // usage example
          int main(){
              std::vector<int> vi{1,2,3,4,5,6,7,8,9};
              auto n = vi.size();
              int* pi = release(vi);
              for(size_t i=0; i<n; ++i)
                  std::cout << pi[i] << ", ";
          
              return 0;
          }
          

          打印1, 2, 3, 4, 5, 6, 7, 8, 9,

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-09-27
            • 1970-01-01
            • 2014-05-24
            • 2021-11-22
            • 2011-07-30
            • 1970-01-01
            • 1970-01-01
            • 2020-12-26
            相关资源
            最近更新 更多