【问题标题】:Is there a bug in GCC 4.7.2's implementation of shared_ptr's (templated) assignment operator?GCC 4.7.2 的 shared_ptr(模板化)赋值运算符的实现是否存在错误?
【发布时间】:2012-12-22 16:36:20
【问题描述】:

我的问题是关于 shared_ptr 的赋值运算符模板在 GCC 4.7.2 中的实现,我怀疑它包含一个错误。

前提 1:C++11 标准

这里是我说的赋值运算符模板的签名:

template<class Y> shared_ptr& operator=(const shared_ptr<Y>& r) noexcept;

来自 C++11 标准 (20.7.2.2.3):

“相当于 shared_ptr(r).swap(*this)。”

换句话说,赋值运算符模板是根据构造函数模板定义的。构造函数模板的签名如下:

template<class Y> shared_ptr(const shared_ptr<Y>& r) noexcept;

来自 C++11 标准 (20.7.2.2.1):

“要求:除非 Y* 可隐式转换为 T*,否则 [...] 构造函数不得参与重载决议。”

前提 2:GCC 4.7.2 的实现:

现在 GCC 4.7.2 的构造函数模板的实现对我来说似乎是正确的(std::__shared_ptrstd::shared_ptr 的基类):

template<typename _Tp1, typename = 
typename std::enable_if<std::is_convertible<_Tp1*, _Tp*>::value>::type>
__shared_ptr(__shared_ptr<_Tp1, _Lp>&& __r) noexcept
    : 
    _M_ptr(__r._M_ptr), 
    _M_refcount()
{
    _M_refcount._M_swap(__r._M_refcount);
    __r._M_ptr = 0;
}

但是,GCC 4.7.2 对赋值运算符模板的实现如下:

template<typename _Tp1>
__shared_ptr& operator=(const __shared_ptr<_Tp1, _Lp>& __r) noexcept
{
    _M_ptr = __r._M_ptr;
    _M_refcount = __r._M_refcount; // __shared_count::op= doesn't throw
    return *this;
}

让我印象深刻的是,这个操作不是根据构造函数模板定义的,也不是swap() 定义的。特别是,普通赋值 _M_ptr = __r._M_ptr 不会产生与通过 std::is_convertible 显式检查类型 _Tp1*_Tp* 的可转换性时相同的结果(可以专门化)。

前提 3:VC10 实现

我注意到 VC10 在这方面确实有一个更符合标准的实现,我认为这是正确的,并且在我的测试用例中表现得符合我的预期(而 GCC 没有):

template<class _Ty2>
_Myt& operator=(const shared_ptr<_Ty2>& _Right)
{
    // assign shared ownership of resource owned by _Right
    shared_ptr(_Right).swap(*this);
    return (*this);
}

问题

GCC 4.7.2 对shared_ptr 的实现是否确实存在错误?我找不到任何关于这个问题的错误报告。

发布脚本

如果你想问我我的测试用例是什么,为什么我会关心这个看似不重要的细节,为什么我似乎暗示我需要专攻std::is_convertible,请这样做在聊天中。这是一个很长的故事,没有办法在不被误解的情况下总结它(及其所有不愉快的后果)。提前谢谢你。

【问题讨论】:

  • 我认为你在那里有错误的构造函数模板,你给出了从不同的原始指针转换的那个,而不是从不同的shared_ptr转换的那个@
  • 即使您可以专门化is_convertible,但除非您保持标准要求的行为,否则不得这样做...
  • [meta.type.synop]/1 “除非另有说明,否则为本子条款中定义的任何类模板添加特化的程序的行为是未定义的。”您是否有一个未定义的测试用例,并且对于 GCC 和 MSVC 的实现表现不同? IIRC 我直接从 Boost 复制了该赋值运算符代码。
  • 我很好奇您的is_convertible 要求。请注意,该标准不需要std::is_convertible&lt;T,U&gt;::value,而是要求它们是可转换的,并且在赋值中隐式检查。提供is_convertible 的特化只能限制使用,因为在测试通过的情况下仍需要进行初始化/分配。另请注意,该标准不要求使用 SFINAE 或使用 std::is_convertible,因此根据该行为,您的程序不可移植。
  • @AndyProwl C++ 有一个通用的“好像”规则:如果位没有按照标准中的规定实现,但表现得“好像”它们是 - 意味着正确的程序无法判断它们'以不同的方式重新实现——这不是实现中的错误。这就是为什么您是否可以在已定义行为的程序中看到不同行为的问题非常相关。

标签: c++ gcc stl c++11 smart-pointers


【解决方案1】:

让我印象深刻的是,这个操作不是根据构造函数模板定义的,也不是swap()

它不需要,它只需要表现得就好像它是用这些术语定义的。

特别是,普通赋值 _M_ptr = __r._M_ptr 不会产生与通过 std::is_convertible 显式检查类型 _Tp1*_Tp* 的可转换性时相同的结果(可以专门化 )。

我不同意:[meta.type.synop]/1 除非另有说明,否则为本子条款中定义的任何类模板添加特化的程序的行为是未定义的。

所以你不能改变is_convertible&lt;Y*, T*&gt; 的含义,如果Y* 可以转换为T*,那么赋值就会起作用,因为(指针和引用计数对象的)两个赋值都是noexcept 结束结果相当于交换。如果指针不可转换,则赋值将无法编译,但 shared_ptr(r).swap(*this) 也会编译失败,所以它仍然是等价的。

如果我错了,请提交错误报告,我会修复它,但我认为符合标准的程序无法检测 libstdc++ 实现与标准要求之间的差异。也就是说,我不会反对将其更改为以swap 的形式实施。当前的实现直接来自Boost 1.32 中的shared_ptr,我不知道Boost 是否仍然以相同的方式执行它,或者它现在是否使用shared_ptr(r).swap(*this)

[完全披露,我是一名 libstdc++ 维护者,主要负责shared_ptr 代码,该代码最初由boost::shared_ptr 的作者慷慨捐赠,然后被我肢解。]

【讨论】:

  • 当前版本的 Boost (1.52.0) 确实将其实现为 this_type(r).swap(*this);。我知道这个实现至少从版本 1.44.0 开始就存在。不确定何时更改。
【解决方案2】:

GCC 中的实现符合标准中的要求。当标准定义一个函数的行为等同于一组不同的函数时,这意味着前者的效果等同于标准中定义的后者函数的效果(而不是实现的)。

标准不要求该构造函数使用std::is_convertible。构造函数需要 SFINAE,但赋值运算符不需要 SFINAE。类型可转换的要求放在程序上,而不是 std::shared_ptr 的实现上,这是你的责任。如果传入的类型是不可转换的,那么它是你程序中的一个错误。如果它们是,那么即使您想通过专门化 is_convertible 模板来禁用使用,实现也必须接受代码。

再一次,专门 is_convertible 来限制 指针 的转换是未定义的行为,因为您正在更改基本模板的语义,而这在标准中是明确不允许的。

这导致了您不想回答的原始问题:让您想到此解决方案的用例是什么。或者换一种说法,为什么人们一直在问解决方案,而不是问他们想要解决的真正问题?

【讨论】:

  • 确实需要该构造函数的 SFINAE:[util.smartptr.shared.const]/17 第二个构造函数不应参与重载决议,除非 @987654325 @ 可以隐式转换为 T* 但它不需要 SFINAE 用于赋值运算符,因此无关紧要。
  • @JonathanWakely:你说得对,我看错了构造函数。
猜你喜欢
  • 2016-03-20
  • 2011-05-23
  • 1970-01-01
  • 2015-08-23
  • 2015-01-27
  • 2021-11-02
  • 2014-02-01
  • 2019-06-30
相关资源
最近更新 更多