Does the C++ standard specify STL implementation d

2019-06-15 16:22发布

问题:

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?

回答1:

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.



回答2:

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.



回答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.



回答4:

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.