从 const 方法返回 std::vector<int*> 时如何传播 const?

How can I propagate const when returning a std::vector<int*> from a const method?

让我们在一个示例中展示它,我们有一个包含主要数据的数据 class,某种指向主要数据的索引,我们还需要公开一个 const 版本指数.

class Data
{
public:
  const std::vector<int>& getPrimaryData() const { return this->primaryData; }
  const std::vector<int*>& getIndex() const { return this->index; }
private:
  std::vector<int> primaryData;
  std::vector<int*> index;
};

这是错误的,因为用户可以轻松修改数据:

const Data& data = something.getData();
const std::vector<int*>& index = data.getIndex();
*index[0] = 5; // oups we are modifying data of const object, this is wrong

这样做的原因是,Data::getIndex 应该 return 的正确类型是:

const std::vector<const int*>&

但是您可以猜到当您尝试将方法编写为“仅将非常量变体转换为常量变体”时会发生什么:

// compiler error, can't convert std::vector<int*> to std::vector<const int*> these are unrelated types.
const std::vector<const int*>& getIndex() const { return this->index; }

据我所知,C++对这个问题没有很好的解决办法。显然,我可以创建新向量,从索引中复制值并 return 它,但从性能角度来看这没有任何意义。

请注意,这只是大型程序中实际问题的简化示例。 int 可以是一个更大的对象(可以说是 Book),而 index 可以是某种书籍的索引。而Data可能需要使用索引来修改书籍,但同时以const的方式提供索引来阅读书籍。

每种语言都有其规则和用法...std::vector<T>std::vector<const T> 在 C++ 中是不同的类型,不可能 const_cast 一个变成另一个,句号。这并不意味着 constness 被打破,它只是意味着它不是它的工作方式。

对于使用部分,返回一个完整的容器通常被视为一种糟糕的封装实践,因为它使实现可见并将其绑定到接口。最好有一个方法获取一个索引并返回一个指向 const 的指针(或者如果需要的话,返回一个指向 const 的指针的引用):

const int* getIndex(int i) const { return this->index[i]; }

这行得通,因为 T* 可以 const_cast 编辑为 const T *

您可以 return 将视图转换为矢量。示例:

auto getIndex() const {
    auto to_const = [](int* ptr) -> const int* {
        return ptr;
    };
    return this->index | std::views::transform(to_const);
}

编辑:std::span


如果 index 包含指向 primaryData 元素的指针,那么您可以通过存储表示当前指向的对象的索引的整数来解决问题。任何有权访问 non-const primaryData 的人都可以轻松地将这些索引转换为指向 non-const 的指针,其他人则不能。

primaryData isn't stable,

如果 primaryData 不稳定,并且 index 包含指向 primaryData 的指针,那么当前的设计将被破坏,因为这些指针将失效。只要索引保持稳定(即你只插入到后面),整数索引替代方案就可以解决这个问题。如果连索引都不稳定,那么你使用的是错误的数据结构。链接列表和链接列表的迭代器向量可以工作。

在 C++20 中,您可以 return 具有 const int*

类型元素的 std::span
#include <vector>
#include <span>

class Data
{
public:
  std::span<const int* const> getIndex() const { return this->index; }
private:
  std::vector<int*> index;
};

int main() {
  const Data data;
  const auto index = data.getIndex();
  *index[0] = 5;  // error: assignment of read-only location
}

Demo

但是,您要求 std::experimental::propagate_const. But since it is an experimental feature, there is no guarantee that any specific toolchain is shipped with an implementation. You may consider implementing your own. There is an MIT licensed implementation。包含 header 后:

using namespace xpr=std::experimental;
///...
std::vector<xpr::propagate_const<int*>> my_ptr_vec;

但是请注意,原始指针被认为是邪恶的,因此您可能需要使用 std::unique_ptrstd::shared_ptrpropagate_const 应该接受智能指针和原始指针类型。

这是一个丑陋的解决方案,适用于 C++20 之前的版本,使用 reinterpret_cast:

const std::vector<const int*>& getIndex() const{ 
    return reinterpret_cast<const std::vector<const int*>&>(data); 
}

请注意,这实际上是 return 绑定到左值的引用,而不是 const& 绑定到右值的引用:

std::vector<const int*>& getIndex() const{ 
    return reinterpret_cast<std::vector<const int*>&>(data); 
}

如果您可以使用 C++20 或更高版本(或 GSL 等库),使用范围或跨度的最佳答案是一个很好的解决方案。如果没有,这里有一些其他方法。

不安全的转换

#include <vector>

class Data
{
public:
  const std::vector<const int>& getPrimaryData() const
  {
    return *reinterpret_cast<const std::vector<const int>*>(&primaryData);
  }

  const std::vector<const int* const>& getIndex()
  {
    return *reinterpret_cast<const std::vector<const int* const>*>(&index);
  }

private:
  std::vector<int> primaryData;
  std::vector<int*> index;
};

这是危险的生活。这是未定义的行为。至少,您不能指望它是便携的。没有什么可以阻止实现为 const std::vector<int>const std::vector<const int> 创建不同的模板重载,这会破坏您的程序。例如,库可能会向非 const 元素的 vector 添加一些额外的 private 数据成员,而对于 constvector 元素则不会元素(无论如何都不鼓励)。

虽然我没有对此进行广泛测试,但它似乎 可以在 GCC、Clang、ICX、ICC 和 MSVC 中工作。

智能数组指针

智能指针的数组特化允许从 std::shared_ptr<T[]> 转换为 std::shared_ptr<const T[]>std::weak_ptr<const T[]>。您可以使用 std::shared_ptr 作为 std::vector 的替代方案,并使用 std::weak_ptr 作为 vector.

视图的替代方案
#include <memory>

class Data
{
public:
  std::weak_ptr<const int[]> getPrimaryData() const
  {
    return primaryData;
  }

  std::weak_ptr<const int* const[]> getIndex()
  {
    return index;
  }

private:
  std::shared_ptr<int[]> primaryData;
  std::shared_ptr<int*[]> index;
};

与第一种方法不同,这是 type-safe。与范围或跨度不同,这从 C++11 开始可用。请注意,您实际上并不希望 return 一个没有数组绑定的不完整类型——这只是在乞求缓冲区溢出漏洞——除非您的客户端通过其他方式知道数组的大小。它主要用于 fixed-size 数组。

子范围

std::span 的一个很好的替代方法是 std::ranges::subrange,您可以将其专门用于数据的 const_iterator 成员类型。这是根据开始和结束迭代器而不是迭代器和大小定义的,甚至可以(经过修改)用于具有 non-contiguous 存储空间的容器。

这在 GCC 11 和 clang 14 和 -std=c++20 -stdlib=libc++ 中有效,但不适用于所有其他编译器(截至 2022 年):

#include <ranges>
#include <vector>

class Data
{
private:
   using DataType = std::vector<int>;
   DataType primaryData;
   using IndexType = std::vector<DataType::pointer>;
   IndexType index;

public:
  /* The types of views of primaryData and index, which cannot modify their contents.
   * This is a borrowed range. It MUST NOT OUTLIVE the Data, or it will become a dangling reference.
   */
  using DataView = std::ranges::subrange<DataType::const_iterator>;
  // This disallows modifying either the pointers in the index or the data they reference.
  using IndexView = std::ranges::subrange<const int* const *>;

  /* According to the C++20 standard, this is legal.  However, not all
   * implementations of the STL that I tested conform to the requirement that
   * std::vector::cbegin is contstexpr.
   */    
  constexpr DataView getPrimaryData() const noexcept
  {
    return DataView( primaryData.cbegin(), primaryData.cend() );
  }

  constexpr IndexView getIndex() const noexcept
  {
    return IndexView( index.data(), index.data() + index.size() );
  }
};

您可以将 DataView 定义为实现范围接口的任何类型,例如 std::spanstd::string_view,客户端代码应该仍然有效。

准备如下类型,并用作Data::getIndex()的return类型。

class ConstIndex
{
private:
  const std::vector<int*> &index;
public:
  ConstIndex( const std::vector<int*> &index ) : index(index) {}

public:
  //Implement methods/types needed to emulate "const std::vector<const int*>"
  const int *operator[]( size_t i ) const { return index[i];    }
  const int *at( size_t i ) const { return index.at(i); }
  ...
};

如评论中所述,您可以这样做:

class Data
{
public:
  const std::vector<int>& getPrimaryData() const { return this->primaryData; }
  const std::vector<const int*>& getIndex() const { return this->index; }
private:
  std::vector<int> primaryData;
  std::vector<const int*> index;
  int* read_index_for_writing(std::size_t i) { return const_cast<int*>(index[i]); }
};

此解决方案的优点:它在标准的每个版本和每个兼容的实现中都有效且安全。它 returns 一个没有有趣包装的矢量引用 类 – 这对调用者来说可能无关紧要,但它可能。

不好:您必须在内部使用 helper 方法,尽管只是在为了写入数据而读取索引时。评论者形容它“脏”,但对我来说它似乎足够干净。