将 const std::unique_ptr 用于 pimpl 习惯用法

Posted

技术标签:

【中文标题】将 const std::unique_ptr 用于 pimpl 习惯用法【英文标题】:Using const std::unique_ptr for pimpl idiom 【发布时间】:2016-09-26 13:00:06 【问题描述】:

在Herb Sutter's talk at CppCon16 中,他建议用const std::unique_ptr 编写pimpl idiom(大约10 分钟)。

这应该如何与移动构造函数/赋值一起使用? c ++ 17中有什么东西吗?我找不到任何东西。

【问题讨论】:

为什么你不希望你的大多数 pimpl 类是可移动的?这似乎是一件非常合理的事情。 @DenisYaroshevskiy 只能假设他有一个特定的用例。一般来说,我同意使用unique_ptr作为pimpl 的容器。如果您希望它是可复制的,那么您当然必须通过克隆操作来实现它。 刚看了视频的相关部分,我不同意他的观点。我认为对于大多数 pimpl 句柄来说,const unique_ptr 很快就会成为一个限制。 @RichardHodges - 萨特是对的。如果你复制一个带有 pimpl 的类,如果 pimpl 是 shared_ptr,你或者有两个具有相同 pimpl 的对象,或者如果 pimpl 是 unique_ptr,则一个没有 pimpl 的对象。这打破了一个对象的合同,一个粉刺。另一方面,带有const pimpl 的对象仍然可以移动。 @SamVarshavchik 我看不出const unique_ptr 是如何移动的。没有const unique_ptr&& 构造函数。如果您可以提供示例代码,我会很感兴趣。我认为您必须编写一个移动构造函数来移动实现。这仍然使一个对象处于未定义状态。值得注意的是,他没有提供const unique_ptr-as-pimpl (non)-idiom 的用例。 Herb 是个聪明人,但他不一定是我会盲目追随的人。 【参考方案1】:

如果您的类应该是永不为空的,那么非常量的唯一 ptr(具有默认移动/分配)是不合适的。 move ctor 和 move assign 都会清空 rhs。

一个 const unique ptr 将禁用这些自动方法,如果你想移动,你必须在 impl 中编写它(并在外面有点胶水)。

我会亲自编写一个具有我想要的语义的值 ptr(然后让编译器编写胶水),但是从 const unique_ptr 开始作为第一遍听起来是合理的。

如果你放宽永不为空,让它几乎永不为空,你现在必须推理很多方法的前提条件,以及可能的连锁错误。

这种技术的最大成本,即返回值的困难,在 C++17 中消失了。

【讨论】:

如果你从一个对象中移出,它应该处于部分形成状态,所以它只能被分配和破坏,所以移动构造函数/赋值不会破坏你的 not_null 保证。如果你愿意,你可以从 gsl 想出类似 not_null 之类的东西来包装它。 @DenisYaroshevskiy 不,它处于您愿意保证的任何状态?它不是“应该处于部分形成的状态”,除非这是您选择 来保证的。一种选择是几乎从不为空,并且只有在移出后才为空。这是一种选择,它有成本和收益。不应该忽视从永不空到几乎永不空的成本。同上从几乎从不空到从不空的成本。永不为空更容易保证正确性,偏向于更经常正确的代码而不是更快的代码通常是一个好主意。 @Yakk 即使移动的对象“从不为空”,在移动后使用它也是一种强烈的代码气味。我认为,pimpl 指针检查后跟断言/异常比允许人们依赖处于“默认”状态的移动对象更可取。当由于不可避免的任务蔓延而无法再满足保证时,这是一场灾难。我什至会说这是一种反模式,与std::move 的精神背道而驰。 @RichardHodges 有时空类型肯定是灾难,因为您在可能为空的上下文中意外使用它们。防止这种情况很难。您可以修复它,但只能通过广泛而全面的 QA(如果您假设完美的 QA,那么没有任何代码是“危险的”)。通过使用const unique ptr,您的对象将无法移动,并且不会发生此问题。通过使用特定的never_empty_ptr,您的代码不能为空,并且不会出现此问题。另一方面,将永不为空的状态手动滚动到类中可能是个坏主意。 @Yakk 如果阻止使用移出对象很难(对于给定的团队?),那么这就要求对一个(可能是不合格的)开发人员强制“不使用std::move”约束.当然,所有优秀的人一旦找到更好的工作就会离开,但你会得到“永不空虚”的保证,同时仍然允许其他团队编写高效的代码。我很难想象这个成语的合理用例。它一口气消除了从 c++03 升级到 c++11 所提供的实用程序和性能的 90% 的巨大增长。【参考方案2】:

这假设如何与移动构造函数/赋值一起使用?

Move constructors:

如果满足以下任一条件,则为类 T 隐式声明或默认的移动构造函数定义为已删除

T 具有无法移动的非静态数据成员(已删除、无法访问或不明确的移动构造函数)

const std::unique_ptr 是这样一个数据成员,因为const

如果const 被删除,编译器会生成移动构造函数和赋值,但不会生成复制构造函数。


Herb 解释了他为什么使用const unique_ptr

非 const 也可以工作,但它更脆弱,因为默认的移动语义可能不正确。

const 成员更健壮,因为const 成员必须在构造函数中初始化。而const 说明对象的实现并没有改变,它不是State 或Strategy 设计模式。

【讨论】:

那么在这种情况下是否必须实现一个复制构造函数? @vordhosbn 如果有必要的话。 没错,但我的问题是关于移动构造/分配。使用普通的 unique_ptr 而不是 const 将保持移动构造/分配的活力和良好,同时声明它 const 禁止它。我曾经认为 const 数据成员是一种不好的做法(因为这个问题),但显然 Herb Sutter 不这么认为。我想知道为什么。 我的观点与萨特先生相反。 std::movestd::unique_ptr 上的默认行为是绝对正确的,也是我们想要的——有效的状态转移,使移动的句柄处于非常明确的状态——“无效,请勿触摸”。明确禁用移动是一回事(人们可能想知道其动机)。通过神秘地使用 const 来禁用它们在我看来就像是在开玩笑。 对我来说,最重要的是通过构造来表达生命周期,并获得正确的默认值(在这种情况下,默认情况下没有脆性移动)。您可以自己编写复制和移动操作,例如MyClass& operator=(const MyClass& that) *pimpl = *that.pimpl; return *this; Impl 类仍然可以复制甚至移动,您可以委托给它。当然,这只能让你完成 95% 的工作,而且你也可以编写自己的 value_ptr 类型来自动执行深层复制/移动。

以上是关于将 const std::unique_ptr 用于 pimpl 习惯用法的主要内容,如果未能解决你的问题,请参考以下文章

为啥非 const 引用必须用左值初始化?

矢量的独特副本

如何将 std::unique_ptr 初始化为引用

将 std::unique_ptr 的子类与 std::variant 一起使用

如何将 std::sort() 与 std::unique_ptr<[]> 一起使用?

std::将 std::unique_ptr 移动到 stl 容器中。 (MSVC 编译器问题)