Search code examples
c++oopreference

std::vector not initialized in initializer list when using reference member


I'm trying to initialize the values vector with 25 values of 3 (for example) using the following code:

#include <vector>
#include <iostream>

class VecClass {
public:
    VecClass(int v) 
        : values(25, v),
        var1(new int(v)), 
        var2(v){}

public:
    std::vector<int> values;      // not initialized
    int* var1;                    // initialized
    int var2;                     // initialized
};

class BaseClass {
public:
    BaseClass(const VecClass& ref) 
        : spec(ref) {}

public:
    const VecClass& spec;
};

int main() {
    BaseClass ref = BaseClass(VecClass(3));
        
        std::cout << "*var1 = " << *ref.spec.var1 << "\n";  // output : 3
        std::cout << "var2 = " << ref.spec.var2 << "\n";    // output : 3
        std::cout << "size of values = " << ref.spec.values.size() << "\n"; // output : 0 (expected : 25)

}

It's working fine for the members var1 and var2 that are initialized to 3. However, it doesn't work for vector member values that should be initialized with 25 elements, all set to 3. I'm using Visual Studio 2022.

I would like to understand why values is not initialized in this context and how to initialize it using BaseClass as showed in the example above.

I've tested with const VecClass& spec = VecClass(3); and it's working fine, so I believe the issue might be related with the BaseClass constructor.


Solution

  • The problem is with the spec reference. It refers to an object. What object does it refer to, where does it "live"? How long does it live? If you answer this, you will find the answer to your question.

    This program makes it more obvious:

    Live On Coliru

    #include <iostream>
    #include <vector>
    
    class VecClass {
      public:
        VecClass(int v) : values(25, v), var1(new int(v)), var2(v) {}
    
      public:
        std::vector<int> values; // not initialized
        int*             var1;   // initialized
        int              var2;   // initialized
    };
    
    class BaseClass {
      public:
        BaseClass(VecClass const& ref) : spec(ref) {}
    
      public:
        VecClass const& spec;
    };
    
    int main() {
        auto* this_obj_needs_to_be_alive = new VecClass(3);
    
        BaseClass ref = BaseClass(*this_obj_needs_to_be_alive);
    
        std::cout << "*var1 = " << *ref.spec.var1 << "\n";                  // output : 3
        std::cout << "var2 = " << ref.spec.var2 << "\n";                    // output : 3
        std::cout << "size of values = " << ref.spec.values.size() << "\n"; // output : 25
    
        delete this_obj_needs_to_be_alive;
    
        // everything beyond this point is undefined behavior
        std::cout << "*var1 = " << *ref.spec.var1 << "\n";                  // output : who knows
        std::cout << "var2 = " << ref.spec.var2 << "\n";                    // output : who knows
        std::cout << "size of values = " << ref.spec.values.size() << "\n"; // output : who knows
    }
    

    This prints something like

    *var1 = 3    
    var2 = 3
    size of values = 25
    *var1 = 3
    var2 = 3
    size of values = 7973933
    

    Of course, the last three lines could print anything, or even crash, or sing the national anthem because indirecting through the reference to the destructed VecClass object is Undefined Behavior.

    The easiest thing here is to avoid references and pointers if you can, and otherwise make sure that the referred-to-object outlives the reference/pointer.

    Trivia

    • Why did var1/var2 appear to work?

    • It's because they're much simpler (primitive) types and they simply "appear" ok because they haven't yet been overwritten in memory. This is purely accidental though.

    • Note how the compiler is able to warn for the simple example I posted, and says:

       warning: pointer used after 'void operator delete(void*, std::size_t)' [-Wuse-after-free]
      
       main.cpp:31:12: note: call to 'void operator delete(void*, std::size_t)' here    
          31 |     delete this_obj_needs_to_be_alive;
      
    • Not all problems will be diagnosed at compiletime, but you'll catch way more if you compile with warnings enabled

    • Catch some of the remaining problems with sanitizers: -fsanitize=undefined,address on Clang and GCC