12

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
bitmask
  • 32,434
  • 14
  • 99
  • 159
  • What makes you think this has anything to do with `shared_ptr`? What happens if you just create some type and use that in place of `shared_ptr`? – Nicol Bolas Sep 16 '12 at 05:24
  • It works in GCC 4.7.1 (except for the comments ending in backslashes, causing multi-line comment problems) http://liveworkspace.org/code/d7713b217a30dc973795698adfd3c1c8, and I believe it is correct. (I must say, though, I find it a bit misleading to use an `operator B::Ptr` to create a new shared-ptr instance of B, since that instance does not seem to be any kind of _converted form of the A instance_. I trust this is because we are dealing with a reduced example.) – jogojapan Sep 16 '12 at 05:27
  • @NicolBolas: Of course I tried exactly that before posting, but that seemed to work fine. I suspect it has to do with the constructor overloads provided by g++'s implementation of the `shared_ptr`. I'm just at a loss to explain why the older version worked. – bitmask Sep 16 '12 at 05:28
  • @jogojapan: Indeed, as I noted in the OP, I tried to trim out everything that wasn't essential to the problem. – bitmask Sep 16 '12 at 05:30
  • This question would be clearer if you gave a custom simplified smart-pointer class instead of shared_ptr. Your code seems to work fine for me using 4.7.0 on MinGW. Without seeing the specific implementation of shared_ptr, it's hard to say definitively 'this is right' or 'this is wrong'. Regarding the issue, it sounds like gcc can't find a constructor for shared_ptr&, which I assume is the type of your operator. There is a constructor for const shared_ptr & tho...maybe try defining operator B::Ptr() const{...}? – Rollie Sep 16 '12 at 06:35
  • @Rollie: But `a` is mutable, why wouldn't the mutating operator be eligible? If I use a custom type instead of a `shared_ptr` I cannot reproduce the issue. *That*'s the question. Would the definition of my `std::shared_ptr` help? – bitmask Sep 16 '12 at 06:40
  • Have you tried `B::Ptr({*a});` or `B::Ptr(*a);`? (mat be the parser can be fooled ...) – Emilio Garavaglia Sep 16 '12 at 06:45
  • @bitmask: you can reproduce this with a custom type; start with the full shared_ptr impl, and just start deleting sections that don't matter, and you will be left with something readable. a is mutable, but I don't believe parameter deduction logic will ever say "I expect an A, but I've been given a C. C can be cast to a B, and B can be cast to an A, so we'll do that". At most, I seem to recall it only does a single cast. If it requires more, it fails. – Rollie Sep 16 '12 at 06:52
  • @EmilioGaravaglia: If I use `()` braces I have to change the expression slightly as it would be a (re-)declaration of `a` otherwise. So, `(B::Ptr(*a))` compiles while `(B::Ptr{*a})` gives the same errors as above. – bitmask Sep 16 '12 at 07:00
  • @Rollie: It *should* expect a `B::Ptr const&` and it is given an `A&&`. `A&&` can be converted to `B::Ptr` so I still don't see the problem. – bitmask Sep 16 '12 at 07:02
  • @Rollie: Throwing out stuff from `shared_ptr` doesn't change anything, as there already seems to be missing something in the `std` version. I get the same error with a different namespace. – bitmask Sep 16 '12 at 07:16
  • @bitmask rolling your own version of the class would just simplify investigation. I take back what I said earlier though, this looks like 4.6.3 is just wrong. *a should be cast (using the user defined operator) to a `B::Ptr &&`, which should bind to the `const & Ptr` constructor without issue. If not, I'm very curious to hear the correct answer :) – Rollie Sep 16 '12 at 07:27

1 Answers1

5

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.

Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • That quote doesn't really address the question at hand. It talks about falling back to non-initiaizer-list constructors after failing to find an initializer-list constructor. But the question at hand is whether it should consider the conversion operator after *also* failing to find a non-initializer-list constructor. – fgp Sep 29 '12 at 13:06
  • @fgp Conversion operators are considered when resolving a constructor call; see above. – ecatmur Oct 01 '12 at 08:58
  • @fgp it gets more interesting; per 13.3.3.1p4 your example is not allowed, although it's a rather strange result. – ecatmur Oct 01 '12 at 13:55