【问题标题】:Is there a way to convert std::vector<const T*> to std::vector<T*> without extra allocations?有没有办法将 std::vector<const T*> 转换为 std::vector<T*> 而无需额外分配?
【发布时间】:2016-10-12 08:31:13
【问题描述】:

假设我有一些Objects 的Storage,它有一个方法可以聚合指向向量中一些Objects 的指针。像这样:

class Storage
{
public:
   std::vector<Object*> aggregate_some_objects(); // non-const version
   std::vector<const Object*> aggregate_some_objects() const; // const version

private:
   std::unordered_map<size_t, Object> m_objects; // data is stored 
                                                 // by-value in a non-vector container
}

一般来说,在实现 const + non-const 方法对时,有一种方法可以避免复制粘贴,方法是在 const_cast 的帮助下调用其中一个方法对。然而,这是不可能的,因为方法的返回类型不同。

此处避免复制粘贴的最直接方法是从非const 版本调用const 版本,并使用返回的std::vector&lt;const T*&gt; 填充单独的std::vector&lt;T*&gt;。然而,这将导致至少 2 个堆分配(每个向量一个)。我想避免与第二个向量相关的分配。

我想知道是否有办法写类似的东西

template <typename T>
std::vector<T*> remove_const_from_vector_of_ptrs(std::vector<const T*>&& input)
{
   std::vector<T*> ret;
   // do some magic stuff here that does not involve
   // more memory allocations
   return ret;
}

因此,允许写入

std::vector<const Object*> Storage::aggregate_some_objects() const
{
   // non-trivial implementation
}

std::vector<Object*> Storage::aggregate_some_objects() 
{
   auto objects = const_cast<const Storage*>(this)->aggregate_some_objects();
   return remove_const_from_vector_of_ptrs(std::move(objects));
}

std::vector(例如std::unique_ptr)中没有允许转移内存所有权的“释放”方法——这是有充分理由的,所以我认为这是不可能的。

我也明白,如果可能的话,这将是一个通常应该避免的危险操作,就像const_cast一样。但在这种情况下谨慎使用似乎比复制粘贴更有益。

编辑: 澄清了“额外”分配的含义,并将Storage::aggregate_objects() 更改为Storage::aggregate_some_objects(),以更好地表明这些方法的实现比基于范围的方法更复杂循环 - 因此希望避免复制粘贴实现。

【问题讨论】:

  • 您的函数按值返回,因此每次都分配一个新向量。您要避免哪些“额外分配”?
  • 是的,函数按值返回,这意味着必须发生至少一个堆分配(最好的情况——我知道返回向量的大小,可以调用vector::reserve)。我正在谈论将在不涉及复制粘贴的相关方法的最简单实现中发生的分配:非const 版本调用const 版本(返回std::vector&lt;const T*&gt;)并填充一个单独的@ 987654341@。创建一个单独的向量将导致至少 1 个(“额外”)分配,这是我想避免的。在问题中澄清了这一点。
  • 如果你的目标只是避免复制函数体,那么不要试图用强制转换和未定义的行为做任何愚蠢的事情,只需编写一个返回 std::vector&lt;T*&gt; 的函数模板,然后用 @ 调用它987654343@ == ObjectT == const Object

标签: c++ c++11 vector stl


【解决方案1】:

简短的回答是:不。 std::vector&lt;Object*&gt;std::vector&lt;const Object*&gt; 是两个不同的独立类。它们彼此不同,就像class A 来自class B。人们通常认为,仅仅因为它们都以std::vector 开头,它们就以某种方式相互关联。这不是真的,因此没有办法“就地”将一个转换为另一个。这些向量类中的每一个都拥有它们对应的内部data(),并且不会心甘情愿地把它交给其他一些奇怪的类。

答案仍然是否定的,但在许多情况下,可以解决此问题以避免手动重复代码。事实上,在大多数此类情况下,代码重复是不可避免的,而能做的最好的事情就是避免手动重复代码。

一种常见的方法是让常量和可变类方法成为单个共享私有模板的外观:

// Header file:

class Storage {

public:

    std::vector<const Object*> aggregate_objects() const;

    std::vector<Object*> aggregate_objects();

private:

    template<typename v_type> void make_aggregate_objects(v_type &v) const;
};

// In the translation unit:

template<typename v_type> void Storage::make_aggregate_objects(v_type &v) const
{
     // Now, create 'v' here... v.reserve(), v.push_back(), etc...
}

std::vector<const Object*> Storage::aggregate_objects() const
{
     std::vector<const Object *> v;

     make_aggregate_objects(v);

     return v;
}

std::vector<Object*> Storage::aggregate_objects()
{
     std::vector<const Object *> v;

     make_aggregate_objects(v);

     return v;
}

编译器仍然会生成两个几乎相同的代码块,但至少不是你来做所有的输入。

另一种类似的方法是将 lambda 传递给模板函数,而不是传递向量对象,而私有模板函数使用 lambda 函数作为回调来构造返回的向量。通过一点类型擦除和std::function 的一些帮助,私有类方法可以转换为普通方法,而不是模板方法。

【讨论】:

  • 这似乎是最简单解决方案的最完整答案。谢谢!只想添加一件事:虽然您称这种方法很常见,但我想指出,如果我遇到这样的代码,我可能会挠头一两秒钟,想为什么v_type 是模板化的。这是否打算用于不同的向量(或者如果make_aggregate_objects 的参数是std::vector&lt;T&gt;&amp; v,则用于完全不同的实体)?我认为 tmlen 提出的std::conditional_t 方法可以澄清这种误解,也许有些人会发现它更有用。
  • @SergeyNikitin - 使用std::conditional_t 的主要优点是强制错误的参数在过程的早期导致编译错误,而不是隐藏在函数内部某处的编译错误,并引用C++ 库容器的一些内部的、未记录的成员,这些成员显然与任何事情无关。我想在破译 C++ 编译错误大约 15 年之后,这对我来说价值很小;并且我可以免费使用其他容器重新调整模板的用途。
【解决方案2】:

如果不重新分配内存和复制指针,就无法将std::vector&lt;const Object*&gt; 转换为std::vector&lt;Object*&gt;,因为std::vector 是一个容器并拥有它的内存。

在这种情况下使用reinterpret_cast 可能会起作用,但它是未定义的行为并且取决于std::vector 的实现:

std::vector<const Object*> const_vec = ...;
std::vector<Object*>& vec = reinterpret_cast<std::vector<Object*>&>(const_vec);

避免const_cast 或不必要的分配的解决方案是第三个模板函数:

template<typename Stor>
static auto Storage::aggregate_objects_(Stor&)
-> std::vector<std::conditional_t<std::is_const<Stor>::value, const Object*, Object*>>
{
    ...
}

其中Stor 可以是Storageconst Storage

那么aggregate_objects() 将被实现为:

std::vector<const Object*> Storage::aggregate_objects() const {
    return aggregate_objects_(*this);
}

std::vector<Object*> Storage::aggregate_objects() {
    return aggregate_objects_(*this);
}

【讨论】:

    【解决方案3】:

    您的函数按值返回,所以无论如何都要分配 - 您在说什么“额外分配”?

    如果您只是在内部存储vector&lt;Object*&gt;,那么解决您的问题很简单:

    std::vector<Object*> Storage::aggregate_objects()
    { return m_data; };
    
    std::vector<const Object*> Storage::aggregate_objects() const
    { return std::vector<const Object*>(m_data.begin(), m_data.end()); }
    

    编辑:回应您更新的问题:

    您不应该为了避免复制和粘贴函数体而编写糟糕的代码!

    没有必要复制函数体,或者编写带有危险或风险转换的糟糕代码,只需使用两个函数都调用的模板,正如 Sam Varshavchik 的回答所示。

    【讨论】:

    • 我已经编辑了原始问题以澄清我所说的“额外”分配是什么意思。我还扩展了代码 sn-ps 以更好地了解数据的存储方式,这有望更好地了解正在解决的问题。
    【解决方案4】:

    显而易见的答案是否定的,std::vector&lt;T*&gt;std::vector&lt;const T*&gt; 是两个不能互换的不同对象。

    然而,有了std::vector内部的知识,应该很容易看出我们是否将T*const T*存储在std::vector中,从它的角度来看都没有关系。

    因此,一种方法是将结果从一种类型强制转换为另一种类型。所以:

    template <typename T>
    std::vector<T*> remove_const_from_result(std::vector<const T*>&& input) {
      return reinterpret_cast<std::vector<T*>&>(input);
    }
    

    这种方法的一个重要警告是,只有当我们对强制转换的容器内部相当有信心时,我们才能这样做。

    请注意,这仍然无助于您原来的问题,即您有 2 个不同的成员函数,而您本质上是const_cast-将一个结果放入另一个。

    为了说明,让我假设你有一个没有std::vector 的 getter 函数,所以拿这个:

    struct Foo {
      const Bar* bar() const { return &bar_; }
      Bar* bar() { return const_cast<Bar*>(bar()); }
    
     private:
      Bar bar_;
    };
    

    IMO,一个更好的方法是在内部公开一个模板化函数,然后使用它。所以,Foo 变成:

    struct Foo {
      const Bar* bar() const { return get_internal<const Bar*>(&bar_); }
      Bar* bar() { return get_internal<Bar*>(&bar_); }
    
     private:
      Bar bar_;
      template <typename T>
      T get_internal(std::remove_const<T>::type ptr) const { return ptr; }
    };
    

    因此,同样,对于您的示例,您可以使用相同的方法:

    struct Storage {
      std::vector<const Object*> aggregate_objects() const { return aggregate_internal<const Object*>(); }
      std::vector<Object*> aggregate_objects() { return aggregate_internal<Object*>(); }
    
     private:
      template <typename T>
      std::vector<T*> aggregate_internal() const {
        // actual aggregate function where T* can be const T* also.
      }
    }
    

    【讨论】:

    • 您的“强制转换”方法无法编译,即使编译也不会避免额外分配,因为它按值返回(与原始方法一样)。
    • “强制转换”示例应为:template&lt;class T&gt; std::vector&lt;T*&gt;&amp; remove_const_from_result(std::vector&lt;const T*&gt;&amp; input) { return reinterpret_cast&lt;std::vector&lt;T*&gt;&amp;&gt;(input); }
    • @JonathanWakely RVO 应确保不进行额外分配。
    • @Arindam:不过,您将其更新为不是我写的内容!您的版本 still 制作了整个向量的额外副本。如果你打算玩这些东西,你应该首先在melpon.org/wandbox 上测试你的代码(使用一些T 来打印关于构造/破坏的消息)。不,乔纳森韦克利在这些问题上不太可能出错。 ;) RVO/copy-elision 不适用,因为input 是一个参数;而且你也不会从input搬走。
    • @Quuxplusone reinterpret_cast 是未定义的行为。我的回答显示了如何在没有 UB 的情况下做到这一点,尝试使用演员阵容是一个非常非常糟糕的主意。直接说不吧。
    猜你喜欢
    • 2019-10-20
    • 2021-06-02
    • 2016-03-31
    • 1970-01-01
    • 2019-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-01
    相关资源
    最近更新 更多