2

Please consider the code below, which compiles in VS2012 but fails in VS2010 with the error

    1>------ Build started: Project: testconstinit, Configuration: Debug Win32 ------
1>  testconstinit.cpp
1>c:\program files (x86)\microsoft visual studio 10.0\vc\include\xmemory(48): error C2248: 'std::unique_ptr<_Ty>::unique_ptr' : cannot access private member declared in class 'std::unique_ptr<_Ty>'
1>          with
1>          [
1>              _Ty=int
1>          ]
1>          c:\program files (x86)\microsoft visual studio 10.0\vc\include\memory(2347) : see declaration of 'std::unique_ptr<_Ty>::unique_ptr'
1>          with
1>          [
1>              _Ty=int
1>          ]
1>          c:\program files (x86)\microsoft visual studio 10.0\vc\include\xmemory(197) : see reference to function template instantiation 'void std::_Construct<std::unique_ptr<_Ty>,const std::unique_ptr<_Ty>&>(_Ty1 *,_Ty2)' being compiled
1>          with
1>          [
1>              _Ty=int,
1>              _Ty1=std::unique_ptr<int>,
1>              _Ty2=const Movable &
1>          ]
1>          c:\program files (x86)\microsoft visual studio 10.0\vc\include\xmemory(196) : while compiling class template member function 'void std::allocator<_Ty>::construct(std::unique_ptr<int> *,const _Ty &)'
1>          with
1>          [
1>              _Ty=Movable
1>          ]
1>          c:\program files (x86)\microsoft visual studio 10.0\vc\include\vector(421) : see reference to class template instantiation 'std::allocator<_Ty>' being compiled
1>          with
1>          [
1>              _Ty=Movable
1>          ]
1>          c:\program files (x86)\microsoft visual studio 10.0\vc\include\vector(481) : see reference to class template instantiation 'std::_Vector_val<_Ty,_Alloc>' being compiled
1>          with
1>          [
1>              _Ty=Movable,
1>              _Alloc=std::allocator<Movable>
1>          ]
1>          c:\users\zadirion\documents\visual studio 2010\projects\testconstinit\testconstinit\testconstinit.cpp(34) : see reference to class template instantiation 'std::vector<_Ty>' being compiled
1>          with
1>          [
1>              _Ty=Movable
1>          ]
1>          c:\users\zadirion\documents\visual studio 2010\projects\testconstinit\testconstinit\testconstinit.cpp(81) : see reference to class template instantiation 'LazyValue<T>' being compiled
1>          with
1>          [
1>              T=Container
1>          ]
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

The code:

#include "stdafx.h"
#include <vector>
#include <memory>
#include <functional>
#include <deque>


using namespace std;



typedef std::unique_ptr<int> Movable;
typedef vector<Movable> Container;

typedef vector<Movable> (*MakeType)();


template <class T, class Initializer = function<T(void)> >
struct LazyValue
{
  LazyValue(Initializer aInit) : mInit(aInit) {}


  void Init() const
  {
    m = mInit();
  }

private:

  mutable T m; // <-- compiler error at this line
  Initializer mInit;

  LazyValue operator=(const LazyValue & aOther)
  {

  }
};

template <class T>
struct GenericList
{
    std::deque<T> mValues;

    GenericList(){}

    GenericList & operator()(T && aValue)
    {
        mValues.push_back(std::move(aValue));
        return *this;
    }

    template <class Container>
    operator Container()
    {
        auto it = mValues.begin();
        auto endIt = mValues.end();

        Container c;

        for ( ; it != endIt; it++ )
        {
            c.push_back(std::move(*it));
        }
        return std::move(c);
    }
};

template <class T>
GenericList<T> ListOfRValues()
{
    return GenericList<T>();
}

int _tmain(int argc, _TCHAR* argv[])
{

  const LazyValue<Container> s = []()->Container{
      return ListOfRValues<Movable>()
        (Movable(new int) )
        (Movable(new int) )
        (Movable(new int) );
  };

  return 0;
}

Can anyone point with a link to the bug submitted to Microsoft maybe, or an explanation on what the compiler bug is actually, I am trying to understand which part of the code exactly is troubling the compiler. Also, what workaround do we have for this?

Thank you!

ildjarn
  • 62,044
  • 9
  • 127
  • 211
EddieBytes
  • 1,333
  • 9
  • 20
  • It's a bug in VS2012, so it would be good to report it. – Jesse Good Feb 19 '13 at 21:46
  • @JesseGood: It's not a bug. See my answer (the compiler is allowed, although not required, to elide the call to the copy constructor). – Andy Prowl Feb 19 '13 at 21:47
  • @AndyProwl: Hmm, I see, I always thought that there needed to be a viable copy or move constructor present even if the compiler elides the call, but maybe I was wrong on that. – Jesse Good Feb 19 '13 at 21:52
  • 1
    @JesseGood: You probably refer to 12.8/32: *"This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided."*. However, this only mentions overload resolution. And the implicitly generated copy-constructor is a viable candidate. Instantiating it causes an error, but it does not need to be instantiated if the call is elided. This is how I understand it, at least. – Andy Prowl Feb 19 '13 at 21:57
  • @AndyProwl: Right that is the part. Note the part that says `must be accessible`, `std::unique_ptr`s copy constructor is declared **private** in MSVC's implementation, hence I believe it is a bug because it is not accessible. – Jesse Good Feb 19 '13 at 22:08
  • @JesseGood: I actually thought about it and I think you're right. I will edit my answer. – Andy Prowl Feb 19 '13 at 22:13
  • @JesseGood: Although, see [this Q&A](http://stackoverflow.com/questions/14756691/copy-initialization-of-simple-raii-wrapper-with-lambda-fails-unexpectedly-under) (and in particular the last sentence of the accepted answer), which is what kept me thinking that there is no bug so far – Andy Prowl Feb 19 '13 at 22:15
  • @JesseGood: I'm actually not sure anymore. To verify accessibility, shall the compiler actually *instantiate* the implicit copy constructor? – Andy Prowl Feb 19 '13 at 22:29
  • 1
    @AndyProwl: The [answer](http://stackoverflow.com/questions/4639576/copy-constructor-is-not-called-for-copy-initialization-or-optimized) to the SO question has the relevant quotes from the standard. `A program is ill-formed if the copy/move constructor or the copy/move assignment operator for an object is implicitly odr-used and the special member function is not accessible.` and `A copy constructor or move constructor is odr-used even if the call is actually elided by the implementation` – Jesse Good Feb 19 '13 at 22:44

1 Answers1

5

This code should not compile.

The problem is in the fact that you are using copy-initialization, which may require (if the compiler is not eliding it) the construction of a temporary object of type LazyValue<Container>, which is then moved into the initialized object s.

From Paragraph 8.5/14 of the C++11 Standard:

The initialization that occurs in the form

T x = a;

as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1) is called copy-initialization. [ Note: Copy-initialization may invoke a move (12.8). —end note ]

Moreover, according to Paragraph 8.5/16:

[...] Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call initializes a temporary of the cv-unqualified version of the destination type. The temporary is a prvalue. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization. In certain cases, an implementation is permitted to eliminate the copying inherent in this direct-initialization by constructing the intermediate result directly into the object being initialized; see 12.2, 12.8.

Let's assume for the moment that your compiler does not elide the copy/move (the compiler is allowed, but not required, to do so).

Your class template doesn't define any move constructor, and the implicitly generated copy constructor will be selected for constructing the object s from the temporary which has been constructed from the lambda on the right side of the initialization.

Unfortunately, your class has a member variable of type Container, which is a container of non-copyable elements. Hence, instantiation of the implicitly generated copy-construction will fail, which explains the error you are getting.

You should use direct-initialization instead:

const LazyValue<Container> s([]() -> Container {
  return ListOfRValues<Movable>()
    (Movable(new int) )
    (Movable(new int) )
    (Movable(new int) );
});

Let's now consider the case where the compiler does choose to elide the copy/move. There is a requirement in the C++11 Standard on this behavior, coming from Paragraph 12.8/32:

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If overload resolution fails, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. —end note ]

The key term here is accessible. The instantiation of the implicitly generated copy-constructor cannot succeed, because the object to be copied contains a non-copyable sub-object; which necessarily makes the copy-constructor inaccessible, because it can never be instantiated. Hence, a conforming compiler shall refuse to compile the code, and I believe this is qualifies as a bug in VS2012.


P.S.: Also, mind the fact that you are violating the so-called Rule of Three (apart from having an overloaded copy-assignment operator that returns nothing, while it should probably return *this).

Community
  • 1
  • 1
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • 'The problem is in the fact that you are using copy-initialization, which may require (if the compiler is not eliding it)'. Can you point to the text on this? I was under the impression that copy-elide should only be done when there *is* in fact a copy that would else have taken place. The text you posted agrees with that view I think. – Johan Lundberg Feb 19 '13 at 22:29
  • @JohanLundberg: That's from 8.5/16, which is quoted later in the answer. I am not sure anymore though, whether the compiler shall only check that a constructor is accessible, or whether it should also be *compilable* (which is not the case here). And whether checking for accessibility requires compiling. – Andy Prowl Feb 19 '13 at 22:30
  • I find it clear that the code is invalid if it would not compile without eliding. Code is either valid according to C++11 or not. Or stated differently: there are valid C++11 compiler implementations that have the right to refuse to compile this. Is there something more to it? – Johan Lundberg Feb 19 '13 at 22:38
  • This was very helpful. Thank you Andy Prowl and Johan Lundberg for clarifying this, and thank you Andy Prowl for the very detailed explanation. – EddieBytes Feb 20 '13 at 06:30