有没有一种方法可以将std::vector<const T*>转换为std::vector<T*>而不需要额外的分配?

13
假设我有一个存储一些对象的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_cast在一个方法中调用另一个方法来避免在实现const +非const方法对时进行复制粘贴。但是在这种情况下不可能这样做,因为这两个方法的返回类型不同。

避免复制粘贴的最直接的方法是从非const版本调用const版本,并使用返回的std::vector<const T*>填充一个单独的std::vector<T*>。但是这将导致至少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那样的'release'方法,允许转移内存所有权,这是有很好的原因的,所以我认为这是不可能的。

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

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


你的函数通过值返回,因此每次都要分配一个新的向量。你想要避免什么“额外的分配”? - Jonathan Wakely
是的,这些函数返回值是按值传递的,这意味着至少需要进行一次堆分配(最好的情况是我知道返回向量的大小并可以调用vector::reserve)。我所说的是在涉及非const版本调用const版本(它返回std::vector<const T*>)并填充单独的std::vector<T*>的问题的最简实现中将发生的分配。创建一个单独的向量将导致至少1个额外的分配,我想避免这种情况。在问题中澄清了这一点。 - Sergey Nikitin
1
如果你的目标只是为了避免重复函数体,那么不要试图使用强制转换和未定义行为来做任何愚蠢的事情,只需编写一个返回 std::vector<T*> 的函数模板,然后使用 T == ObjectT == const Object 调用它即可。 - Jonathan Wakely
4个回答

13
简短的回答是:不行。 std::vector<Object*>std::vector<const Object*> 是两个不同的、独立的类。它们彼此之间的差异就像 class Aclass 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<T>& v,则用于完全不同的实体)?我认为tmlen提出的std::conditional_t方法可以消除这种误解,并且可能有些人会发现它更有用。 - Sergey Nikitin
@SergeyNikitin - 使用std::conditional_t的主要优势是可以在过程的早期阶段强制错误参数导致编译错误,而不是在函数内部某个地方隐藏的编译错误,并引用一些与C++库容器的内部未记录成员似乎没有任何关系的内容。我想,在解密C++编译错误约15年后,这对我来说几乎没有价值;并且我可以免费重新使用模板与其他容器。 - Sam Varshavchik

2

如果想要将std::vector<const Object*>转换为std::vector<Object*>,必须重新分配内存并复制指针,因为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);
}

1
你的函数总是返回值,因此始终需要分配内存 - 你所说的“额外分配”是什么?
如果你只是在内部简单地存储vector<Object*>,那么解决你的问题就很容易:
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的答案所示。


我已经编辑了原始问题,以澄清我所说的“额外”分配的含义。此外,我扩展了代码片段,以更好地了解数据存储方式,这将有助于更好地了解正在解决的问题。 - Sergey Nikitin

0

显而易见的答案是否定的,std::vector<T*>std::vector<const T*>是两个不同的对象,不能互换使用。

然而,如果了解std::vector内部的知识,就很容易看出,无论我们在std::vector中存储T*还是const T*,从它的角度来看都没有关系。

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

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

这种方法的一个重要限制是,我们只有在相当自信于容器内部结构时才能进行强制转换。
请注意,这仍然无法解决您最初的问题,即您有两个不同的成员函数,并且您本质上是将一个函数的结果进行了const_cast转换到另一个函数中。
为了说明这一点,让我假设您有一个没有std::vector的getter函数,因此可以采用以下方式:
struct Foo {
  const Bar* bar() const { return &bar_; }
  Bar* bar() { return const_cast<Bar*>(bar()); }

 private:
  Bar bar_;
};

更好的方法,我认为,是在内部公开一个模板化函数,并使用它。 因此,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.
  }
}

2
你的“强制转换”方法无法编译,并且即使它能编译,也无法避免额外的分配,因为它返回值(就像原始代码一样)。 - Jonathan Wakely
“Force cast”示例应该是:template<class T> std::vector<T*>& remove_const_from_result(std::vector<const T*>& input) { return reinterpret_cast<std::vector<T*>&>(input); } - Quuxplusone
@JonathanWakely RVO应确保不进行额外的分配。 - Arindam
@Arindam:你更新了一些不是我写的东西!你的版本仍然会多复制整个向量。如果你要玩这些东西,你应该先在http://melpon.org/wandbox/上测试你的代码(使用一些在构造/析构时打印消息的`T`)。而且,Jonathan Wakeley在这些问题上极不可能出错。;) RVO / copy-elision不适用,因为“input”是一个参数;而且你也没有从“input”中移动任何东西。 - Quuxplusone
@Quuxplusone,使用reinterpret_cast是未定义行为。我的回答展示了如何在没有UB的情况下完成它,尝试使用转换来完成它是一个非常糟糕的想法。坚决说不。 - Jonathan Wakely
虽然这是未定义行为(可能会破坏基于类型的别名,导致实际运行中出现真正的问题,因此它不仅仅是理论上的)。但是Arindam当前的代码甚至没有实现任何效率上的优势来“弥补”这个缺点;它与复制整个向量完全相同,但是带有UB的额外缺点。 - Quuxplusone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接