Consider this minimal example:
#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 ----+
}
This innocent copy construction of a shared_ptr<B>
fails horribly on g++ 4.6.3 x86_64-linux-gnu but appears to work for g++ 4.5 (note that the newer version breaks, while the older works!). From what I can tell from the error (see below) g++ 4.6 seems to pass A
by value, instead of by (r or l) reference.
So, the question is, which is correct and which is broken? Is this behaviour supposed to fail? If so, why?
As far as I understand conversion rules, the implicit cast to B::Ptr
should be attempted at this point, right?
Note: I reduced this example to the bare technical problem, and this code doesn't make sense for any production-system as it stands.
Here is the precise error:
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
The code is incorrect under the current version of the standard (I'm looking at post-standard draft n3376).
The rules for list-initialization specify:
13.3.1.7 Initialization by list-initialization [over.match.list]
1 - When objects of non-aggregate class type
T
are list-initialized [...]:
- If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class
T
and the argument list consists of the elements of the initializer list.
However, when overload resolution is applied to the copy constructor of B::Ptr
taking the single parameter const std::shared_ptr<B> &
, the argument list is (*a)
, consisting of a single element of type lvalue A
; overload resolution is not permitted to consider the conversion function A::operator B::Ptr
:
13.3.3.1 Implicit conversion sequences [over.best.ics]
4 - However, when considering the argument of a constructor or user-defined conversion function that is a candidate [...] by 13.3.1.7 [...] when the initializer list has exactly one element and a conversion to some class X or reference to (possibly cv-qualified) X is considered for the first parameter of a constructor of X [...], only standard conversion sequences and ellipsis conversion sequences are considered.
So g++-4.6 is correct to reject this code; g++-4.7.2 unfortunately accepts it, which is incorrect.
The correct way to write this would be to use direct-initialization (B::Ptr(*a)
) or a static_cast<B::Ptr>
.
The restriction on the allowable conversions can be traced to paper n2672, although in that paper the paragraph 13.3.3.1p4 only applies to the argument of a user-defined conversion function. The additional restriction on constructors was added in defect 978:
978. Incorrect specification for copy initialization
13.3.3.1 [over.best.ics] paragraph 4 says,
[...]
This is not quite right, as this applies to constructor arguments, not just arguments of user-defined conversion functions.
The current wording of 13.3.3.1p4 can be traced to the seminal defect 84, which introduced the "common-law rule that only a single user-defined conversion will be called to do an implicit conversion".
I'm a bit uneasy about this answer; I've asked Is it possible to invoke a user-defined conversion function via list-initialization? to see if anyone can clarify the intent of the standard here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With