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

kov*_*rex 36 c++ const-correctness

让我们在一个示例中展示它,其中我们有一个包含主数据的 Data 类、某种指向主数据的索引,并且我们还需要公开索引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;
};
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

原因是 Data::getIndex 应返回的正确类型是:

const std::vector<const int*>&
Run Code Online (Sandbox Code Playgroud)

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

// 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; }
Run Code Online (Sandbox Code Playgroud)

据我所知,C++对于这个问题还没有什么好的解决方案。显然,我可以创建新的向量,从索引复制值并返回它,但从性能角度来看这没有任何意义。

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

康桓瑋*_*康桓瑋 50

在 C++20 中,您可以只返回 astd::span类型的元素const int*

#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
}
Run Code Online (Sandbox Code Playgroud)

演示

  • 此方法适用于*任何*连续范围。如果您不使用连续范围来存储索引,则基于指针的其他答案也将不起作用(就像您接受的那样)。 (8认同)

Ser*_*sta 19

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

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

const int* getIndex(int i) const { return this->index[i]; }
Run Code Online (Sandbox Code Playgroud)

这是可行的,因为 aT*可以被 const_casted 为 a const T *

  • @kovarex ...在这里你给出了一个简单的例子,我只回答了这个例子。恕我直言,如果您有一个包含所有用例的完整示例,并且想知道如何改进它,您可以尝试将其发布到 [Code Review](https://codereview.stackexchange.com/) 以获得详尽的信息审查。 (6认同)
  • @kovarex:你在某一点上是对的:增加不必要的复杂性总是不好的。在现实世界中,这甚至是经验丰富的分析师和开发人员的力量:在界面的复杂性、实现的复杂性和严格遵守最佳实践之间找到正确的平衡。规则不是盲目遵守的,但没有充分理由而忽视规则**是不好的...... (4认同)
  • 我对这种不好的做法持不同意见。我认为恰恰相反,因为不好的做法是将内部容器的功能解压到 API 中,从而使类接口变得臃肿。换句话说,想象一下,索引不仅仅是一个 std::vector&lt;int*&gt;,而是一些复杂索引系统的 3 层结构。您会将所有可以访问它的方式复制到 Data 类的表层吗?例如,我不能再在索引上使用 std::find_if 和类似的方法,如果我想这样做,我必须在 Data 类中实现迭代器、开始/结束方法,所有这些都是为了允许正确的... (2认同)

Dav*_*lor 9

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

\n

不安全演员表

\n
#include <vector>\n\nclass Data\n{\npublic:\n  const std::vector<const int>& getPrimaryData() const\n  {\n    return *reinterpret_cast<const std::vector<const int>*>(&primaryData);\n  }\n\n  const std::vector<const int* const>& getIndex()\n  {\n    return *reinterpret_cast<const std::vector<const int* const>*>(&index);\n  }\n\nprivate:\n  std::vector<int> primaryData;\n  std::vector<int*> index;\n};\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

智能数组指针

\n

智能指针的数组专门化允许从std::shared_ptr<T[]>std::shared_ptr<const T[]>或进行转换std::weak_ptr<const T[]>。您也许可以使用std::shared_ptr作为 的替代方案std::vectorstd::weak_ptr的视图的替代方案vector

\n
#include <memory>\n\nclass Data\n{\npublic:\n  std::weak_ptr<const int[]> getPrimaryData() const\n  {\n    return primaryData;\n  }\n\n  std::weak_ptr<const int* const[]> getIndex()\n  {\n    return index;\n  }\n\nprivate:\n  std::shared_ptr<int[]> primaryData;\n  std::shared_ptr<int*[]> index;\n};\n
Run Code Online (Sandbox Code Playgroud)\n

与第一种方法不同,这是类型安全的。与范围或跨度不同,它自 C++11 起就可用。请注意,您实际上并不想返回没有数组绑定的不完整类型\xe2\x80\x94,\xe2\x80\x99s 只是乞求缓冲区溢出漏洞\xe2\x80\x94,除非您的客户端通过以下方式知道数组的大小其他一些手段。它主要对固定大小的数组有用。

\n

子范围

\n

一个很好的替代方案std::span是 a std::ranges::subrange,您可以专门研究const_iterator数据的成员类型。这是根据开始和结束迭代器定义的,而不是迭代器和大小,甚至可以用于(经过修改)具有非连续存储的容器。

\n

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

\n
#include <ranges>\n#include <vector>\n\nclass Data\n{\nprivate:\n   using DataType = std::vector<int>;\n   DataType primaryData;\n   using IndexType = std::vector<DataType::pointer>;\n   IndexType index;\n\npublic:\n  /* The types of views of primaryData and index, which cannot modify their contents.\n   * This is a borrowed range. It MUST NOT OUTLIVE the Data, or it will become a dangling reference.\n   */\n  using DataView = std::ranges::subrange<DataType::const_iterator>;\n  // This disallows modifying either the pointers in the index or the data they reference.\n  using IndexView = std::ranges::subrange<const int* const *>;\n\n  /* According to the C++20 standard, this is legal.  However, not all\n   * implementations of the STL that I tested conform to the requirement that\n   * std::vector::cbegin is contstexpr.\n   */    \n  constexpr DataView getPrimaryData() const noexcept\n  {\n    return DataView( primaryData.cbegin(), primaryData.cend() );\n  }\n\n  constexpr IndexView getIndex() const noexcept\n  {\n    return IndexView( index.data(), index.data() + index.size() );\n  }\n};\n
Run Code Online (Sandbox Code Playgroud)\n

您可以定义DataView为实现范围接口的任何类型,例如 astd::spanstd::string_view,并且客户端代码应该仍然可以工作。

\n

  • @user541686 而且,需要明确的是,“危险”并不是你会因为不是一个优秀的小程序员而陷入某种麻烦。您的代码可能会在不同版本的编译器或库上崩溃。理论上,语言标准的存在就是为了防止这种情况发生。 (2认同)

eer*_*ika 8

您可以将变换视图返回到向量。例子:

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

编辑:std::span更简单的选项


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

primaryData不稳定,

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


Red*_*ave 7

你要求的是std::experimental::propagate_const. 但由于它是一个实验性功能,因此不能保证任何特定的工具链都会附带实现。您可以考虑实施自己的。然而,有一个MIT 许可的实现。包含标题后:

using namespace xpr=std::experimental;
///...
std::vector<xpr::propagate_const<int*>> my_ptr_vec;
Run Code Online (Sandbox Code Playgroud)

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

  • `但请注意,原始指针被认为是邪恶的` 不,事实并非如此。`您可能需要使用 std::unique_ptr 或 std::shared_ptr` 他们不应该使用它们,除非指针拥有。 (11认同)
  • @kovarex `typedef`、`using`、`decl_type`。`std::vector::reference`。有很多方法可以让生活变得更轻松。出于同样的原因,我将名称空间别名为缩写。 (4认同)
  • @kovarex `auto&amp; index = data.getIndex();` (2认同)
  • @kovarex `使用 index_t = std::experimental:::propagate_const&lt;int*&gt;` 和 `const std::vector&lt;Data::index_t&gt;&amp; index = data.getIndex()` (2认同)

ben*_*nrg 5

正如评论中提到的,你可以这样做:

\n
class Data\n{\npublic:\n  const std::vector<int>& getPrimaryData() const { return this->primaryData; }\n  const std::vector<const int*>& getIndex() const { return this->index; }\nprivate:\n  std::vector<int> primaryData;\n  std::vector<const int*> index;\n  int* read_index_for_writing(std::size_t i) { return const_cast<int*>(index[i]); }\n};\n
Run Code Online (Sandbox Code Playgroud)\n

该解决方案的优点是:它在标准的每个版本和每个合规实施中都有效且安全。它返回一个向量引用,没有有趣的包装类 \xe2\x80\x93 ,这对调用者来说可能并不重要,但它可能。

\n

不好:您必须在内部使用辅助方法,尽管仅在出于写入数据的目的而读取索引时。评论者将其描述为“肮脏”,但对我来说似乎足够干净。

\n