首页 > 解决方案 > 如何编写 const 传播指针类型包装器?

问题描述

为了更好地理解 C++ 类型系统,我努力编写了一个指针包装类,它传播类似于 std::experimental::propagate_const 的常量:

template <typename Pointee> class Ptr {
public:
  Ptr() = delete;

  explicit Ptr(Pointee *);

  Ptr(const Ptr<Pointee> &) = delete;
  Ptr(Ptr<Pointee> &&);

  Ptr<Pointee> &operator=(const Ptr<Pointee> &) = delete;
  Ptr<Pointee> &operator=(Ptr<Pointee> &&);

  ~Ptr()  = default;

  const Pointee *operator->() const;
  Pointee *operator->();
  const Pointee &operator*() const;
  Pointee &operator*();

private:
  Pointee *mPtr;
};

包装器旨在提供接近原始指针的行为,同时还强制执行一种“深”的 const 正确性并防止无意的别名。

为此,删除了复制构造函数和复制赋值运算符:

  1. 通过从 Ptr 复制来防止指向对象的无意别名。
  2. 通过将 const Ptr 复制到非 const Ptr 中来防止对 const 指向的对象进行非 const 访问。

然而,上述设计有两个不幸的后果。

  1. 一个 const Ptr 不能移动到一个 const 或非 const Ptr 中。当 C++17 的强制 RVO 不适用时,这意味着无法从函数返回 const Ptr 对象。
  2. 由于 C++17 的强制复制/移动省略,在某些情况下,即使不存在这样的可行构造函数,也可以从 const Ptr 构造非常量 Ptr。例如,下面的代码可以编译得很好(为了演示的目的,忽略内存泄漏/raw new):
const Ptr<int> allocateImmutableInt(int val) { return Ptr<int>(new int(val)); }


void foo() {
  Ptr<int> immutableInt = allocateImmutableInt(0); // Initializes non-const Ptr from const Ptr 
  *immutableInt = 100;  // Oops, changed value of 'immutable' object
}

第一个问题可以通过引入一个接受 const 右值引用的 move ctor 来部分解决(尽管这感觉有点奇怪和不习惯):

  Ptr(const Ptr<Pointee> &&);

然而,这实际上恶化了第二个问题。现在,即使没有强制移动/复制省略,也可以将 const Ptr 构造为非常量 Ptr。据我所知,要解决这个问题,我们需要一个所谓的“const 构造函数”,即一个只能被调用以生成 const 对象的构造函数:

  Ptr(const Ptr<Pointee>&&) const;

即使 c++ 支持这样的构造函数,第二个问题仍然存在,因为 c++17 在决定初始化对象时是否可以应用强制移动/复制省略时特别忽略了 cv 限定和构造函数的可行性。目前似乎没有办法让 c++ 在将强制复制/移动省略应用于对象初始化之前检查复制/移动是否可行。

据我所知, std::experimental::propagate_const 也存在同样的问题。我想知道我是否遇到了 c++ 的基本限制,或者我是否错误地设计了 Ptr 包装器?我知道这些问题很可能可以通过创建两种类型来消除,即用于非常量访问的 Ptr 和用于仅 const 访问的 ConstPtr。但是,这首先破坏了创建 const-propagating 包装器的目的。

也许我刚刚偶然发现了迭代器类型和 const_iterator 类型都存在的原因。

标签: c++c++17

解决方案


Ptr<int> const allocateImmutableInt(int val) {
    return Ptr<int>(new int(val));
}

不做你认为它做的事。它不会创建一个 const 限定的对象,然后将其传递给immutableInt. - 这甚至是不可能的,因为你删除了复制构造函数。- 相反,编译器会推断

Ptr<int> immutableInt = allocateImmutableInt(0);

作为

Ptr<int> immutableInt(new int(val));

如果您将主要功能编写为:

void foo() {
    Ptr<int> const immutableInt = allocateImmutableInt(0);
    Ptr<int> mutableInt = std::move(immutableInt);  // doesn't compile
    *mutableInt = 100;
}

您将看到正确的行为:move由于 const 是不可能的,并且复制构造函数被删除。

编辑:顺便说一句,你的Ptr课看起来很像std::unique_ptr. 因此你可以写:

#include <memory>
void foo() {
    std::unique_ptr<int> const immutableInt = std::make_unique<int>(0);
    std::unique_ptr<int> mutableInt = std::move(immutableInt);  // doesn't compile
    *mutableInt = 100;
}

如果你想要一个 const 限定的指针,写

std::unique_ptr<int const>

推荐阅读