从隐式强制转换复制shared_ptr有什么问题?



考虑这个最小的例子:

#include <memory>
struct B {
typedef std::shared_ptr<B> Ptr;
};
struct A {
operator B::Ptr() { // type conversion operator                  <----+
return std::make_shared<B>();  //                                   |
}                                //                                   |
};                                 //                                   |
//                                   |
int main() {                       //                                   |
A* a = new A;                    //                                   |
B::Ptr{*a}; // copy construction from a's implicit cast to B::Ptr ----+ 
}

shared_ptr<B>的这种无辜复制构造在g++4.6.3 x86_64-linux-gnu上严重失败,但似乎适用于g++4.5(请注意,新版本会崩溃,而旧版本可以工作!)。根据我从错误(见下文)中可以看出,g++4.6似乎通过了A的值,而不是通过(r或l)引用。

那么,问题是,哪个是正确的,哪个是错误的?这种行为应该失败吗?如果是,为什么
根据我对转换规则的理解,此时应该尝试对B::Ptr进行隐式转换,对吗?


注意:我把这个例子简化为一个赤裸裸的技术问题,这个代码对任何生产系统来说都没有意义

这是准确的错误:

shp.cpp: In function ‘int main()’:
shp.cpp:17:12: error: no matching function for call to ‘std::shared_ptr<B>::shared_ptr(<brace-enclosed initializer list>)’
shp.cpp:17:12: note: candidates are:
/usr/include/c++/4.6/bits/shared_ptr.h:315:2: note: template<class _Alloc, class ... _Args> std::shared_ptr::shared_ptr(std::_Sp_make_shared_tag, const _Alloc&, _Args&& ...)
/usr/include/c++/4.6/bits/shared_ptr.h:266:17: note: constexpr std::shared_ptr<_Tp>::shared_ptr(std::nullptr_t) [with _Tp = B, std::nullptr_t = std::nullptr_t]
/usr/include/c++/4.6/bits/shared_ptr.h:266:17: note:   no known conversion for argument 1 from ‘A’ to ‘std::nullptr_t’
/usr/include/c++/4.6/bits/shared_ptr.h:258:2: note: template<class _Tp1, class _Del> std::shared_ptr::shared_ptr(std::unique_ptr<_Up, _Ep>&&)
/usr/include/c++/4.6/bits/shared_ptr.h:253:2: note: template<class _Tp1> std::shared_ptr::shared_ptr(std::auto_ptr<_Tp1>&&)
/usr/include/c++/4.6/bits/shared_ptr.h:248:11: note: template<class _Tp1> std::shared_ptr::shared_ptr(const std::weak_ptr<_Tp1>&)
/usr/include/c++/4.6/bits/shared_ptr.h:236:2: note: template<class _Tp1, class> std::shared_ptr::shared_ptr(std::shared_ptr<_Tp1>&&)
/usr/include/c++/4.6/bits/shared_ptr.h:226:7: note: std::shared_ptr<_Tp>::shared_ptr(std::shared_ptr<_Tp>&&) [with _Tp = B, std::shared_ptr<_Tp> = std::shared_ptr<B>]
/usr/include/c++/4.6/bits/shared_ptr.h:226:7: note:   no known conversion for argument 1 from ‘A’ to ‘std::shared_ptr<B>&&’
/usr/include/c++/4.6/bits/shared_ptr.h:218:2: note: template<class _Tp1, class> std::shared_ptr::shared_ptr(const std::shared_ptr<_Tp1>&)
/usr/include/c++/4.6/bits/shared_ptr.h:206:2: note: template<class _Tp1> std::shared_ptr::shared_ptr(const std::shared_ptr<_Tp1>&, _Tp*)
/usr/include/c++/4.6/bits/shared_ptr.h:184:2: note: template<class _Deleter, class _Alloc> std::shared_ptr::shared_ptr(std::nullptr_t, _Deleter, _Alloc)
/usr/include/c++/4.6/bits/shared_ptr.h:165:2: note: template<class _Tp1, class _Deleter, class _Alloc> std::shared_ptr::shared_ptr(_Tp1*, _Deleter, _Alloc)
/usr/include/c++/4.6/bits/shared_ptr.h:146:2: note: template<class _Deleter> std::shared_ptr::shared_ptr(std::nullptr_t, _Deleter)
/usr/include/c++/4.6/bits/shared_ptr.h:129:2: note: template<class _Tp1, class _Deleter> std::shared_ptr::shared_ptr(_Tp1*, _Deleter)
/usr/include/c++/4.6/bits/shared_ptr.h:112:11: note: template<class _Tp1> std::shared_ptr::shared_ptr(_Tp1*)
/usr/include/c++/4.6/bits/shared_ptr.h:103:7: note: std::shared_ptr<_Tp>::shared_ptr(const std::shared_ptr<_Tp>&) [with _Tp = B, std::shared_ptr<_Tp> = std::shared_ptr<B>]
/usr/include/c++/4.6/bits/shared_ptr.h:103:7: note:   no known conversion for argument 1 from ‘A’ to ‘const std::shared_ptr<B>&’
/usr/include/c++/4.6/bits/shared_ptr.h:100:17: note: constexpr std::shared_ptr<_Tp>::shared_ptr() [with _Tp = B]
/usr/include/c++/4.6/bits/shared_ptr.h:100:17: note:   candidate expects 0 arguments, 1 provided

在当前版本的标准下,代码是不正确的(我正在查看标准后草案n3376)。

列表初始化规则指定:

13.3.1.7通过列表初始化[over.match.list]进行初始化

1-当非聚合类类型T的对象被列表初始化时[…]:

  • 如果找不到可行的初始值设定项列表构造函数,则会再次执行重载解析,其中候选函数是类T的所有构造函数,参数列表由初始值设定值列表的元素组成

然而,当重载解析应用于采用单个参数const std::shared_ptr<B> &B::Ptr的复制构造函数时,参数列表是(*a),由类型为lvalueA的单个元素组成;过载分辨率不允许考虑转换函数A::operator B::Ptr:

13.3.3.1隐式转换序列[over.best.ics]

4-然而,当考虑构造函数或用户定义的转换函数的自变量时,该自变量是13.3.1.7[…]的候选者[…],当初始值设定项列表只有一个元素,并且X的构造函数的第一个参数考虑到对某个类X的转换或对X的引用(可能是cv限定的)[…]时,仅考虑标准转换序列和省略号转换序列。

因此g++-4.6拒绝此代码是正确的;g++-4.7.2不幸接受了它,这是不正确的。

正确的写入方式是使用直接初始化(B::Ptr(*a))或static_cast<B::Ptr>

对允许转换的限制可以追溯到n2672号文件,尽管在该文件中,第13.3.3.1p4段仅适用于用户定义转换函数的自变量。在缺陷978:中增加了对构造函数的额外限制

978.复制初始化的规范不正确

13.3.3.1[over.best.ics]第4段说,
[…]
这不太正确,因为这适用于构造函数参数,而不仅仅是用户定义转换函数的参数。

13.3.3.1p4的当前措辞可以追溯到精液缺陷84普通法规则,即只调用单个用户定义的转换来进行隐式转换";。

我对这个答案有点不安;我问过,是否可以通过列表初始化调用用户定义的转换函数?看看是否有人能在这里澄清标准的意图。

最新更新