How does std::list store variables of different types in C+

Prerequisite knowledge

Virtual function memory layout

First we have the following simple C++ code

class X {<!-- -->
    int x;
    string str;

public:
    X() {<!-- -->}
    virtual ~X() {<!-- -->}

    virtual void printAll() {<!-- -->}
};

class Y : public X {<!-- -->
    int y;

public:
    Y() {<!-- -->}
    ~Y() {<!-- -->}

    void printAll() {<!-- -->}
};

Then we give the memory layout of object of class Y

 | |
      |------------------------------| <------ Y class object memory layout
      |int X::x|
stack |------------------------------|
  | | int string::len |
  | |string X::str ----------------|
  | | char* string::str |
 \|/ |------------------------------| |-------|----- ---------------------|
      | X::_vptr |------| | type_info Y |
      |-----------------------------| |----------------- ---------|
      | int Y::y | | address of Y::~Y() |
      |-----------------------------| |----------------- ---------|
      | o | | address of Y::printAll() |
      | o | |--------------------------|
      | o |
------|---------------------------------|--------
      | X::X() |
      |---------------------------------| |
      | X::~X() | |
      |---------------------------------| |
      | X::printAll() | \|/
      |---------------------------------| text segment
      | Y::Y() |
      |---------------------------------|
      | Y::~Y() |
      |---------------------------------|
      | Y::printAll() |
      |---------------------------------|
      | string::string() |
      |---------------------------------|
      | string::~string() |
      |---------------------------------|
      | string::length() |
      |---------------------------------|
      | o |
      | o |
      | o |
      | |

We can see that class Y inherits class X, so the object of class Y also has a virtual pointer of class X (X::_vptr), and class Y overloads the virtual function of class X, So the virtual pointer (X::_vptr) in class Y points to the overloaded function of class Y (Y::~Y(),Y: :printAll()), what about the object of class X? The virtual pointer (X::_vptr) in the object of class X actually points to Y::~Y (), Y::printAll(), because it is overloaded by the derived class class Y

The derived class pointer is passed to the base class

The most common example is that the base class sets the destructor as a virtual function. Let’s see what happens if the base class destructor does not add virtual in the following code

#include <iostream>

class base{<!-- -->
public:
    base(){<!-- --> std::cout << "base construct" << std::endl; }
    ~base(){<!-- --> std::cout << "base destruct" << std::endl; }

};

class derive : public base{<!-- -->
public:
    derive(){<!-- --> std::cout << "derive construct" << std::endl; }
    ~derive(){<!-- --> std::cout << "derive destruct" << std::endl; }
};

int main(){<!-- -->
    base* b = new derive;
    delete b;
}
base construct
derive construct
base destruct

Why only the base class will be destructed but not the derived class when delete b? What we new is a derived class (including members of the derived class and members of the base class). If only the base class is destructed, then the members of the derived class will stay in memory and cause a memory leak…

Because we are destroying the base class, and the base class object (we pass the derived class object to the base class, which means we can only use the base class part of the object) did not find the destructor of the derived class, so only the base class will be destroyed , at this time we add the keyword virtual to the destructor of the base class, which means that the virtual function pointed to by vptr(base::_vptr) in the base class object is re- load, and the virtual function includes the destructor of the base class. At this time, the destructor is overloaded as the destructor of the derived class, that is, derive::~derive(), then the base The class has a destructor of the derived class so that the part of the derived class in the memory can be destructed, see the following code

#include <iostream>

class base{<!-- -->
public:
    base(){<!-- --> std::cout << "base construct" << std::endl; }
    virtual ~base(){<!-- --> std::cout << "base destruct" << std::endl; }

};

class derive : public base{<!-- -->
public:
    derive(){<!-- --> std::cout << "derive construct" << std::endl; }
     ~derive(){<!-- --> std::cout << "derive destruct" << std::endl; }
};

int main(){<!-- -->
    base* b = new derive;
    delete b;
}

output

base construct
derive construct
derive destruct
base destruct

Implementation

How to store different types of elements in the list we discussed today also uses the above principle, store different types of variables in the derived class, and store the base class type in std::list The pointer to the std::list container is the derived class object. At this time, we can access different types of objects in the derived class through the virtual function of the base class (overloaded by the derived class). As the following code

#include <iostream>
#include <memory>
#include <string>
#include <list>

class Container{<!-- -->

public:
    Container(const std::string &Key)
    :Key(Key){<!-- -->

    };
    virtual ~Container(){<!-- -->};
public:
    std::string get_Key() const {<!-- --> return Key; }
    virtual void get_Value(){<!-- -->};
private:
    std::string Key;
};

template<typename T>
class Member : public Container{<!-- -->
public:
    Member(std::string Key, T Value)
    :Container(Key), Value(Value){<!-- -->

    };
    virtual ~Member(){<!-- -->};
public:
    void get_Value() const {<!-- --> std::cout << Value << std::endl; }

private:
    T Value;
};

int main(){<!-- -->

    std::list<Container> c;
    c.push_back(Member<int>("key1", 2));
    c.push_back(Member<char>("key2",'a'));
    c.push_back(Member<bool>("key3",true));
    
    return 0;
}

There is no big problem in terms of design, because it has been explained before, here is a summary of the pitfalls at the language level (mainly virtual functions)

  1. When we write virtual functions, if we use pure virtual functions (virtual func() = 0; this type) in the class, then this class is an abstract class. If it is an abstract class, it means that we Cannot instantiate this class, what we can do is let it be the base class, let the derived class overload, such as the following code
class A{<!-- -->
public:
A();
virtual ~A();
virtual void print() = 0; //virtual function
};

class B{<!-- -->
public:
B();
~B();
void print();
}

class C : public A{<!-- -->
public:
C();
~C();
void print();
}

class D : public A{<!-- -->
public:
D();
~D();
void print();
virtual void print2() = 0; //virtual function
}

int main(){<!-- -->
A a; // error!!!
D d; //error!!!
B b;
C c;
return 0;
}

In the above code, class A and class D are abstract classes so they cannot be instantiated so A a; and D d; are wrong, they can only be used for other base class of class

  1. When we use virtual functions, we may encounter such an error Undefined Reference to vtable ..., which essentially tells us that our virtual functions are only declared without definition…and we use them later For him, look at the following code
class A{<!-- -->
public:
A();
virtual ~A(){<!-- -->};
virtual void print(); //Only declare without definition
};

class B{<!-- -->
public:
B();
~B();
void print(){<!-- -->/****/};
};

In the above code, the virtual function is only declared, but not defined, so in the eyes of the compiler, it means that it is in a compilation unit, and the specific definition of the virtual function is elsewhere (because we only have declarations here), but we do not define it, so the linker An error will be reported. When we write the project code, the class file is usually placed in the header file .h and the method of the class is placed in the corresponding .cc file. They form a compilation unit, so when we instantiate class A, we will An error is reported, because the compiler finds that the virtual function print in A may be overloaded, in other words used, but the compiler does not find the definition of the virtual function print in class A, so the compiler thinks that the definition is in another file (corresponding to .cc) At this time, the compiler will look for it from the same compilation unit, and then link, but because it is not found, the link is wrong, and the solution is very simple, that is, turn the declaration into a definition, virtual void print() ; can be changed to virtual void print(){};, another example is very similar to the above problem is the following code

extern int i;
int *pi = &i;

We declare i, tell the compiler that the definition of i is outside the file, and then we continue to use this external variable i. When compiling, the compiler will find this variable i from other files. Normally, it will find and link together, but not found, so an error will be reported, which is exactly the same as our above problem