Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do I see unusual behavior when using std::vector with a default constructor?

Summary

I have seen a few questions on std::vector recently, and out of curiosity I have been playing around with them a little. I've never really used the STL much, but I knew you could use vector to deal with the allocation of arrays of objects, and I could have sworn there was a way to use the default constructor to allocate the items within when the vector is created. Indeed, this question Initializing a std::vector with default constructor deals with initializing a vector using either a copy constructor and default value vs. just using the default constructor.

However, as I have been doing some experimenting in Visual Studio 2010 with a C++ Console Application project, I have not been getting results consistent with this explanation. According to one of the comments in the answer to the aforementioned question (given here), if you use, e.g., std::vector<FooClass> FooArray = new std::vector<FooClass>(20); it should use the default constructor, and this was indeed the behavior I expected.

However I wrote some tracing code to track objects as they were created, assuming they would be created with the default constructor, and it appeared that every object was just created and subsequently immediately destroyed. Finally after much searching here, there, and everywhere, I went ahead and implemented a copy constructor that printed out information as well. What I am seeing is that if I initialize a vector of FooClass using a default value, with, e.g., new std::vector<FooClass>(20, FooClass()), then I get the expected result: a FooClass() is instantiated, each of the items in the vector is initialized with the copy constructor as a copy of that object, and then the value used as the default is destroyed.

But, if I do new std::vector<FooClass>(20), instead of using the default constructor, it seems to be doing something that's a little (to me) bizarre. Twenty times, a temporary FooClass object is created using the default constructor, an element of the array is constructed via the copy constructor using the temporary, and then the temporary is destroyed.

This really just doesn't make sense to me; but I wonder if perhaps I was just doing something wrong.

The Code

FooClass.h

#include <stdio.h>

class FooClass
{
public:
    FooClass()
    {
        printf("Foo %i Created!\n", NumFoos);

        myFooNumber = FooClass::NumFoos;
        ++FooClass::NumFoos;
        myIsACopy = false;
    }

    FooClass(const FooClass& Another)
    {
        printf("Foo %i (a copy of Foo %i) Created!\n", FooClass::NumFoos, Another.myFooNumber);

        myFooCopiedFrom = Another.myFooNumber;
        myFooNumber = FooClass::NumFoos;
        ++FooClass::NumFoos;
        myIsACopy = true;
    }

    void PrintMe()
    {
        if (myIsACopy)
            printf("I'm Foo %i (a copy of Foo %i)!\n", myFooNumber, myFooCopiedFrom);
        else
            printf("I'm Foo %i!\n", myFooNumber);
    }

    ~FooClass()
    {
        printf("Foo %i Deleted!\n", myFooNumber);
    }

private:
    int myFooCopiedFrom;
    int myFooNumber;
    bool myIsACopy;

private:
    static int NumFoos;

};

FooClass.cpp

#include "FooClass.h"

int FooClass::NumFoos = 0;

FooVector.cpp

// FooVector.cpp : Defines the entry point for the console application.

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

#include "FooClass.h"

//#define USE_INITIALIZER

int _tmain(int argc, _TCHAR* argv[])
{
#ifdef USE_INITIALIZER
    std::vector<FooClass> myFooArray =
        std::vector<FooClass>(5, FooClass());
#else
    std::vector<FooClass> myFooArray =
        std::vector<FooClass>(5);
#endif

    for (int i=0; i < 5; ++i)
        myFooArray[i].PrintMe();

    printf("We're done!\n");

    return 0;
}

Output with default initializer

    Foo 0 Created!
    Foo 1 (a copy of Foo 0) Created!
    Foo 2 (a copy of Foo 0) Created!
    Foo 3 (a copy of Foo 0) Created!
    Foo 4 (a copy of Foo 0) Created!
    Foo 5 (a copy of Foo 0) Created!
    Foo 0 Deleted!
    I'm Foo 1 (a copy of Foo 0)!
    I'm Foo 2 (a copy of Foo 0)!
    I'm Foo 3 (a copy of Foo 0)!
    I'm Foo 4 (a copy of Foo 0)!
    I'm Foo 5 (a copy of Foo 0)!
    We're done!

Output with no initializer

    Foo 0 Created!
    Foo 1 (a copy of Foo 0) Created!
    Foo 0 Deleted!
    Foo 2 Created!
    Foo 3 (a copy of Foo 2) Created!
    Foo 2 Deleted!
    Foo 4 Created!
    Foo 5 (a copy of Foo 4) Created!
    Foo 4 Deleted!
    Foo 6 Created!
    Foo 7 (a copy of Foo 6) Created!
    Foo 6 Deleted!
    Foo 8 Created!
    Foo 9 (a copy of Foo 8) Created!
    Foo 8 Deleted!
    I'm Foo 1 (a copy of Foo 0)!
    I'm Foo 3 (a copy of Foo 2)!
    I'm Foo 5 (a copy of Foo 4)!
    I'm Foo 7 (a copy of Foo 6)!
    I'm Foo 9 (a copy of Foo 8)!
    We're done!

The Question

So... Am I setting up my class improperly and this is the expected behavior? Is this perhaps a quirk of Microsoft's implementation of the STL?

Or is there some other explanation entirely?

Final Note

I removed the sgi specs and comments thereon, because, as James's answer pointed out, the sgi specification is not the actual specification. See, for instance, the resources for wikipedia's entry on C++ for links to working drafts of the real specs. Thanks everyone! :)

like image 810
shelleybutterfly Avatar asked Aug 21 '11 04:08

shelleybutterfly


2 Answers

This is a bug in the Visual C++ 2010 Standard Library implementation. This was also discussed in Standard library containers producing a lot of copies on rvalues in GCC.

The correct behavior depends on the version of the C++ specification your compiler and libraries are designed to.

In C++98/C++03 (what was the "current" C++ specification until last week), both versions of your code would call the same std::vector constructor, which is declared as:

vector(size_type n, const T& x = T(), const Allocator& = Allocator());

The constructor makes n copies of x into the vector. If you don't explicitly provide a T object to be copied, one is constructed implicitly via the default argument. If you compile your code using Visual C++ 2008, you will find that your code has this behavior regardless whether you declare USE_INITIALIZER. In both cases, you'll get the "Output with default initializer" result that you show.

In C++11 (current as of last week), the behavior is changed. This constructor has been split into two distinct constructors:

vector(size_type n, const T& x, const Allocator& = Allocator()); // (1)
vector(size_type n);                                             // (2)

(1) is used if you explicitly provide an object to be copied and n copies of x are made into the vector. (2) is used if you do not provide an object to be copied: n objects of type T are value initialized / default constructed in the vector. No copies at all are made.

So, with a C++11 implementation, if you declare USE_INITIALIZER, you'll get the same behavior as in C++03. If you do not declare USE_INITIALIZER, you should get the following output.

Foo 0 Created!
Foo 1 Created!
Foo 2 Created!
Foo 3 Created!
Foo 4 Created!
I'm Foo 0!
I'm Foo 1!
I'm Foo 2!
I'm Foo 3!
I'm Foo 4!
We're done!

Visual C++ 2010 does not correctly implement the C++11 std::vector constructors and it ends up creating and destroying a bunch of objects that it shouldn't. This should be fixed in a future version of Visual C++.

like image 193
James McNellis Avatar answered Sep 18 '22 01:09

James McNellis


The standard allocator provides a construct method that is used (internally by std::vector) to construct objects on it's buffer. As far as I understand it there is no default constructor version, only a copy constructor version:

// construct a new object at address _ptr, by copying from _obj
allocator::construct(pointer _ptr, const_ref _obj)

In the second case, where you have not provided an object to the vector's constructor to copy from, a new temporary must be instantiated each time construct is called, something like:

// obviously simplified, but to construct the ith object in the vector
allocator::construct(&vector_buffer[i], FooClass());

I think the standard allows extra constructors/destructors to be called, so there's nothing wrong going on, but you could definitely argue that it's sub-optimal.

Hope this helps.

like image 26
Darren Engwirda Avatar answered Sep 19 '22 01:09

Darren Engwirda