10

While writing an answer to this question I faced an interesting situation - the question demonstrates the scenario where one would want to put a class in an STL container but fails to do so because of a missing copy constructor/move constructor/assignment operator. In this particular case the error is triggered by std::vector::resize. I made a quick snippet as a solution and saw another answer that had provided a move constructor instead of an assignment operator and copy constructor as I had. What was interresting that the other answer did not compile in VS 2012, while clang/gcc were happy with both approaches.

First:

// Clang and gcc are happy with this one, VS 2012 is not
#include <memory>
#include <vector>

class FooImpl {};

class Foo
{
    std::unique_ptr<FooImpl> myImpl;
public:
    Foo( Foo&& f ) : myImpl( std::move( f.myImpl ) ) {}
    Foo(){}
    ~Foo(){}
};

int main() {
    std::vector<Foo> testVec;
    testVec.resize(10);
    return 0;
}

Second:

// Clang/gcc/VS2012 are all happy with this
#include <memory>
#include <vector>

using namespace std;
class FooImpl {};

class Foo
{
    unique_ptr<FooImpl> myImpl;
public:
    Foo()
    {
    }
    ~Foo()
    {
    }
    Foo(const Foo& foo)
    {
        // What to do with the pointer?
    }
    Foo& operator= (const Foo& foo)
    {
        if (this != &foo)
        {
            // What to do with the pointer?
        }
        return *this;
    }
};

int main(int argc, char** argv)
{
    vector<Foo> testVec;
    testVec.resize(10);
    return 0;
}

To understand what was happening I looked at the STL sources in VS 2012 and saw that it really was invoking the move assignment operator so that is why my sample worked (I don't have a Linux machine accessible to understand what is going on in clang/gcc) and the other did not, since it had only the move copy constructor.

So this created the following question - can the compiler freely decide how to implement STL methods (in this case std::vector::resize), since radically different implementations could cause non-portable code? Or is this simply a VS 2012 bug?

Community
  • 1
  • 1
Rudolfs Bundulis
  • 11,636
  • 6
  • 33
  • 71
  • [There are opened standard PDFs](http://stackoverflow.com/a/4653479/2729109), (The "real" standard paper is not free;) but I recommend to start from easier site like [cppreference.com](en.cppreference.com/w/cpp). Standard is too hard to start :(. Of course, like our case, there are some cases that cppref is differ from the original standard. You should notice it. – ikh Jun 19 '15 at 12:42
  • This was a bug. C++11 introduced new fine-grained container requirements, which we had not completely implemented in Visual C++ 2012. We fixed many bugs in this area in Visual C++ 2013 (about 60 or so--most of which were found via a near-comprehensive test that we wrote), and we've fixed a few additional issues in Visual C++ 2015. If I recall correctly, there are still a few bugs related to allocator requirements that we have yet to fix. Conformance is a gradual process. – James McNellis Jun 19 '15 at 14:18

4 Answers4

6

Visual C++ 2012 is unable to auto-generate the move constructor and the move assignment operator. A defect that will only be fixed in the upcoming 2015 version.

You can make your first example compile by adding an explicit move assignment operator to Foo:

#include <memory>
#include <vector>

class FooImpl {};

class Foo
{
    std::unique_ptr<FooImpl> myImpl;
public:
    Foo( Foo&& f ) : myImpl( std::move( f.myImpl ) ) {}
    // this function was missing before:
    Foo& operator=( Foo&& f) { myImpl = std::move(f.myImpl); return *this; }
    Foo(){}
    ~Foo(){}
};

int main() {
    std::vector<Foo> testVec;
    testVec.resize(10);
    return 0;
}

As explained in detail by ikh's answer, the standard actually does not require a move assignment operator here. The relevant concepts for vector<T>::resize() are MoveInsertable and DefaultInsertable, which would have been met by your initial implementation with just the move constructor.

The fact that VC's implementation also requires the move assignment here is a different defect, which was already fixed in VS2013.

Thanks to ikh and dyp for their insightful contributions in this matter.

Community
  • 1
  • 1
ComicSansMS
  • 51,484
  • 14
  • 155
  • 166
  • 1
    Thanks for pointing out that this is a known issue, otherwise I would have had happily entered a bug in Microsoft Connect:) – Rudolfs Bundulis Jun 19 '15 at 10:57
  • RValue reference support in VS is a bit tricky. They started supporting it in VS2010 (back then still on the cutting edge of C++0x support), but only with 2015 we finally get a fully conforming implementation. I've seen this issue in particular bite many people that were used to gcc/clang-level of conformance and it can be quite tricky to figure out if you are not already familiar with the history of the feature in VC++. – ComicSansMS Jun 19 '15 at 11:05
  • since your answer points out that this is a VS 2012 imperfection to be fixed in the future I'll gladly add my vote to it, but since the question was more about the standard imposing requirements, do you want to edit it a bit to include the requirements of `std::vector::resize`? – Rudolfs Bundulis Jun 19 '15 at 11:34
  • @RudolfsBundulis I'm actually not sure about this part. I agree with everything in [ikh's answer](http://stackoverflow.com/a/30936261/577603), but I'm not sure whether the fact that VC's implementation wants both move constructor and move assignment is required by the standard. For instance, Clang [compiles an example where Foo has just a move constructor but the move assignment is deleted](http://coliru.stacked-crooked.com/a/59903509ea36c8c5). – ComicSansMS Jun 19 '15 at 12:04
  • @dyp Is the move constructor required though? I could not deduce that from the requirement of MoveInsertible and DefaultInsertible, but clang seems to fail if I delete the move constructor as well. – ComicSansMS Jun 19 '15 at 12:16
  • 1
    MoveInsertable is formulated using the allocator. The default allocator will call either the move constructor or the copy constructor (this depends on their existence and their noexcept-ness). So indirectly, for the default allocator, `vector::resize` after LWG 2033 requires a move constructor or copy constructor (simplified). – dyp Jun 19 '15 at 12:18
  • @dyp Very informative, thanks. I will update the answer accordingly. – ComicSansMS Jun 19 '15 at 12:20
5

Above all, since c++11, std::vector<> can store not-copyable types. (example) Let's take a look at cppreference.

Until c++11, T should be copyable as you know.

T must meet the requirements of CopyAssignable and CopyConstructible.

However, in c++11, The requirements is completely changed.

The requirements that are imposed on the elements depend on the actual operations performed on the container. Generally, it is required that element type is a complete type and meets the requirements of Erasable, but many member functions impose stricter requirements.

.. Erasable is:

The type T is Erasable from the Container X if, given

A the allocator type defined as X::allocator_type

m the lvalue of type A obtained from X::get_allocator()

p the pointer of type T* prepared by the container

the following expression is well-formed:

std::allocator_traits<A>::destroy(m, p);

And look at the "Type requirements" of std::vector::resize() reference:

T must meet the requirements of MoveInsertable and DefaultInsertable in order to use overload (1).

So T doesn't need to be copyable - it only needs to destroyable, movable and default construct-able.

Moreover, since c++14, the restriction of complete type is removed.

The requirements that are imposed on the elements depend on the actual operations performed on the container. Generally, it is required that element type meets the requirements of Erasable, but many member functions impose stricter requirements. This container (but not its members) can be instantiated with an incomplete element type if the allocator satisfies the allocator completeness requirements.

Therefore, I think it's because of just poor standard-conforming of VS2012. It has some defect on latest C++ (e.g. noexcept)


C++11 standard paper N3337 says

void resize(size_type sz);

Effects: If sz <= size(), equivalent to erase(begin() + sz, end());. If size() < sz, appends sz - size() value-initialized elements to the sequence.

Requires: T shall be CopyInsertable into *this.

Therefore in strict c++11, you cannot use std::vector::resize() in this case. (you can use std::vector, though)

However, it is a standard defect and fixed in C++14. and I guess many compilers work well with non-copyable types because copying isn't need to implementing std::vector::resize() indeed. Although VS2012 doesn't work, it's because another bug of VS2012 as @ComicSansMS answered, not because of std::vector::resize() itself.

Community
  • 1
  • 1
ikh
  • 10,119
  • 1
  • 31
  • 70
  • 1
    Hm... cppreference doesn't seem to agree with the standard I cited in my comment on Tartan's post (C++11, 23.3.6.3 ). – Peter - Reinstate Monica Jun 19 '15 at 11:03
  • @PeterSchneider the overload (2) version of `std::vector::resize()` requires CopyInsertable. Didn't you see this? – ikh Jun 19 '15 at 11:06
  • @PeterSchneider Hum, [n3337](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf) says as you say, but I think it is and should be wrong. Let me ask a new question in SO.. – ikh Jun 19 '15 at 11:17
  • 1
    The standard sentence I cited refers to the overload with just a size_t parameter, which is "overload(1)" in the cppreference page (where cppreference disagrees) and the one the OP is using. ["overload (2)" on the cppreference page would be the one with a const T reference. Btw, the standard doesn't seem to impose additional restrictions on that one besides some exception behavior.] – Peter - Reinstate Monica Jun 19 '15 at 11:19
  • @PeterSchneider here's my question: http://stackoverflow.com/questions/30937208/stdvectorresizesize-type-requires-copyinsertable – ikh Jun 19 '15 at 11:38
  • The CopyAssignable requirement from C++03 is a bit silly: It is just a simplification for the implementer. `erase` can assign if you erase some elements from the front, but this doesn't ever happen here. That is, even though CopyAssignable is required, the implementation will *never actually assign*. – dyp Jun 19 '15 at 12:14
  • @dyp `this doesn't ever happen here` In C++03, there's no moving semantics. then Can't that happen if erasing elements from the front as you say? – ikh Jun 19 '15 at 12:32
  • 1
    @ikh No, the effects are specified as *equivalent* to some erase-or-insert operation; `vector::erase` specifies the number of times the copy ctor and assignment op are called. In our case, we erase all elements, so the copy assignment op is never called. `insert` OTOH isn't specified as precisely, but I think there's no reason for `insert` to call the assignment op when appending elements (to the end). Similarly in C++11. – dyp Jun 19 '15 at 12:58
  • 1
    Pretty sure you meant `standard defect` instead of `standard defeat`. Auto correction or Freudian slip? – nwp Jun 19 '15 at 13:26
3

VS2012 is a C++ compiler with some C++11 features. Calling it a C++11 compiler is a bit of a stretch.

Its standard library is very C++03. Its support for move semantics is minimal.

By VS2015, the compiler remains a C++11 with some C++11 features, but its support for move semantics is much better.

VS2015 still lacks full C++11 constexpr support and has incomplete SFINAE support (what they call "expression SFINAE") and some knock-on library failures. It also has deficiencies in non-static data-member initializers, initializer lists, attributes, universal character names, some concurrency details, and its preprocessor is not compliant. This is extracted from their own blog.

Meanwhile, modern gcc and clang compilers have completed C++14 support and have extensive C++1z support. VS2015 has limited C++14 feature support. Almost all of its C++1z support is in experimental branches (which is fair).

All 3 compilers have bugs on top of the features they support.

What you are experiencing here is that your compiler isn't a full C++11 compiler, so your code does not work.

In this case, there was a defect in the C++11 standard as well. Defect reports are usually fixed by compilers and folded into "C++11 compiling mode" by compilers, as well as being incorporated into the next standard. The defect in question was obvious enough that basically everyone who actually implemented the C++11 standard ignored the defect.


The C++ standard mandates certain observable behavior. Often these mandates restrict compiler writers to certain narrow implementation space (with minor variations) assuming a decent quality of implementation.

At the same time, the C++ standard leaves a lot of freedom. The type of iterators into C++ vectors could be a raw pointer under the standard, or a reference-counting smart indexer that generates extra errors when used incorrectly, or something else entirely. Compilers can use that freedom to have their debug builds be instrumented with extra error checking (catching undefined behavior for the programmers), or use that freedom to try different tricks that could grant extra performance (a vector that stores its size and capacity in the allocated buffer could be smaller to store, and usually when you ask for size/capacity you are going to access the data shortly anyhow).

The limitations are usually around data lifetime and complexity bounds.

Usually some reference implementation is written, its limitations and complexity bounds are analyzed, and those are put forward as a restriction. Sometimes parts are left "looser" than the reference implementation requires, which gives freedom to the compiler or library writers.

As an example, there have been some complaints that the unordered map types in C++11 where over-constrained by the standard, and prevent innovation that could permit a more efficient implementation. If fewer constraints where put on said containers, different vendors could experiment, and a faster container might have been converged on instead of the current design.

The downside is that revisions to the standard library break binary compatibility really easily, so if constraints where later added that ruled out some implementations, compiler writers and users could be very annoyed.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    Read the new update: http://blogs.msdn.com/b/vcblog/archive/2015/06/19/c-11-14-17-features-in-vs-2015-rtm.aspx Seems pretty full featured to me. – ForeverLearning Jun 19 '15 at 19:13
  • @Dilip expression sfinae and preprocessor missing from C++11. C++14 sfinae bugs, variable templates, constexpr, aggregate defaults. And they fixed their `std::future`. But yes, that looks better. Still not a C++11 compiler, however. A compiler is actually a C++11 compiler when they are not missing C++11 features. Before that, they are a C++ compiler with some (or even many) C++11 features. – Yakk - Adam Nevraumont Jun 19 '15 at 19:21
1

The C++ standard dictates constraints on T for pretty much all library container functions.

For example, in draft n4296, the constraints for T for std::vector::resize defined in [vector.capacity]/13 are.

Requires: T shall be MoveInsertable and DefaultInsertable into *this.

I don't have access to the final standards for the various versions of C++ to hand for comparison, but I would assume that VS 2012 is non-conformant in its C++11 support in this example.

TartanLlama
  • 63,752
  • 13
  • 157
  • 193
  • ISO/IEC 14882:2011(E) says in "23.3.6.3 vector capacity" about the element type T wrt `void resize(size_type sz)`: `Requires: T shall be CopyInsertable into *this.` – Peter - Reinstate Monica Jun 19 '15 at 10:48