【问题标题】:How can I write a const propagating pointer type wrapper?如何编写 const 传播指针类型包装器?
【发布时间】:2020-10-04 15:33:29
【问题描述】:

为了更好地理解 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 进行非 const 访问 通过将 const Ptr 复制到非 const Ptr 来指向对象。

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

  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 传播包装器的目的。

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

【问题讨论】:

  • 您说“下面的代码可以编译得很好”,但我不相信。见godbolt.org/z/fa4EPn
  • 确实不能用 c++14 或更低版本编译。但是,如果我们在 Godbolt 中添加 -std=c++17 作为参数,由于保证复制/移动省略,它确实可以成功编译。
  • 我假设allocateImmutableInt 应该指向一个不可变的数据,所以Ptr&lt;const int&gt;

标签: c++ c++17


【解决方案1】:

您正在寻找实际上并不存在的问题。

  1. 防止对 const 进行非 const 访问 通过将 const Ptr 复制到非 const Ptr 来指向对象。

这不应该是一个目标,因为它违背了传播const 的想法。

传播const 有两个方面。首先,当指针是const-qualified 时,对象也是const-qualified。你已经涵盖了这个方面。其次,当指针 not const-qualified 时,对象使用其自然限定。也就是说,如果您可以将const Ptr 复制到非const Ptr,则该更改会传播到对象,可能会使对象也非const。这是期望的传播,而不是要阻止的东西。

请记住const 传播的一个主要用例:类成员。为成员指针传播const 通过在const 限定成员函数中生成指向数据const 有助于确保常量正确性。您想象的问题不适用于此用例。不要让情况变得过于复杂。

我知道这些问题很可能可以通过创建两种类型来消除,即用于非常量访问的 Ptr 和用于仅 const 访问的 ConstPtr。

这不是必需的。如果即使指针不是const,对象也应该保持const,那么类型应该是Ptr&lt;const T&gt;而不是Ptr&lt;T&gt;。 例如,您的“不可变 int”应该类似于以下内容。

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

const 已被移动以限定 int,使 int 不可变,无论 const 限定 Ptr

此外,您可能会注意到new int(val) 返回一个int*,它会为您的构造函数隐式转换为const int*。您可能希望为Ptr 复制此隐式转换。像 Ptr(Ptr&lt;std::remove_const&lt;Pointee&gt;&gt; &amp;&amp;) 这样的构造函数可以解决问题,只要它仅在 Pointeeconst 限定时定义(以避免与常规移动构造函数发生冲突)。

【讨论】:

    【解决方案2】:
    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));
    

    如果你把你的 main 函数写成:

    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>
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-01-12
      • 1970-01-01
      • 2013-11-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多