Self-Implementation of C++17 Any Class

Self-Implementation of C++17 Any Class

1. Introduction

The Any class is a standard class introduced in C++17, included in the header file , with the core functionality of storing a single piece of data of any type, while the type does not need to be determined at compile time. It implements type erasure, making it a safe generic type container. This article mainly introduces the self-implementation of the Any class, focusing on reviewing and applying some knowledge points from C++11, while also listing some common mistakes. The implementation code is provided at the end of the article, with detailed comments for error correction and reference.

2. Any Class

2.1 Framework Overview

The overall framework of the Any class consists of three parts: an abstract base class holder object, a placeholder object that inherits from this object, and a member variable that is a pointer to the holder.

To be honest, I initially had two questions that I could not understand:

  1. Why does the Any class need to define an abstract base class? Can't we just use the placeholder directly?\n\n  2. Why is the only member variable in the Any class a pointer to an abstract base class holder? Can't we just use a pointer to the placeholder?

In fact, both of these questions stem from a lack of understanding of the Any class’s “type erasure” feature and C++ polymorphism.

Type erasure here means that the Any class needs to manage various types of data, and after storing data of type A, it should also be able to discard the data of type A and store data of type B. Therefore, Any itself cannot be a specific template for a certain type. Additionally, there cannot be just one class template for the placeholder in Any, because if it were a class template, its type would be fixed after initialization, which would not satisfy the subsequent “type erasure” property. Thus, it is necessary to manage multiple classes generated by a class template. This resolves the first question. It can be seen that this requirement perfectly fits the properties of C++ polymorphism, which allows the use of a pointer to a parent class to call child class objects. When the child class is a class template, one parent pointer can call multiple types of child classes, achieving a one-to-many relationship and unifying the interface and polymorphism. This resolves the second question.

In summary, the framework structure of the Any class is to define an abstract parent class and a class template subclass internally. This allows the use of a parent class pointer to manage and discard multiple types of subclass objects, thus achieving “type erasure”.

2.2 Detailed Analysis

This section mainly revolves around the code, focusing on organizing and reviewing unfamiliar knowledge points while analyzing common mistakes in the code.

Common operations for the class include {universal constructor, default constructor}, {move constructor, move assignment}, {copy constructor, copy assignment}, {destructor}; for the class methods, there are {function templates Get for different types}, {method Type to get the data type stored in the Any class}; for the nested classes, {abstract base class holder (implementing Clone, Type methods)}, {subclass template placeholder (implementing Clone, Type methods)}.

2.21 Construction and Destruction of Any

Implement two versions of the constructor: the default constructor and the universal constructor.

The default constructor is simple, initializing the only abstract base class pointer to zero, but if not careful, it can easily lead to a null pointer situation. It can also be initialized to nullptr when defining the pointer. The corresponding destructor is also simple; if the pointer is not nullptr, it releases the object.

    // Universal constructor\n    template<class T> // decay_t<T> is a method provided by C++ to downgrade the type, ensuring that Any stores the correct type\n    Any(T&& val):_content(new PlaceHolder<std::decay_t<T>>(std::forward<T>(val))){};

Regarding the universal constructor, there is much to discuss. The type operations here are highly demanding; if std::decay_t is not used, errors will occur when passing types like int[], int&, or const int. This is because the object created with new is not the expected type of int, int*, or int, but by using decay_t, the passed type can be “decayed” to achieve the desired type. Initially, std::decay_t was not included, but during testing, it was found that the case of int arr[10] always failed, which was resolved by consulting the documentation. The reason for using perfect forwarding is that the expression of the rvalue reference is an lvalue reference, and we want it to remain an rvalue in the next layer to utilize move semantics and reduce overhead.

Note:
  • • The default constructor must be explicitly written: if a universal constructor is written, the compiler will not generate a default default constructor.
  • • The pointer can be initialized at the time of definition: after C++11, all types of non-static member variables and static const of integral and enumeration types can be assigned an initial value at the time of definition, which can avoid many troubles.
  • • decay_t mainly provides a way to “decay” the passed type, allowing it to reach the target type, which plays a significant role in template metaprogramming and type extraction. Here, it is type extraction. Decay can remove const, references &, volatile (the value of the variable may be modified by factors outside the program, and it must be truly fetched from memory to avoid compiler optimization), and convert int[] types into pointers and function types void(int) into void(*)(int).

2.22 Methods and Nested Classes of Any

The self-implemented Any class provides two interfaces: one for obtaining the Type of the stored object and another for directly manipulating the data through the Get method, allowing users to operate on the data via the returned pointer.

 // Returns a pointer to the data stored in the subclass object\n    template<class T>\n    T* Get(){\n        assert(_content != nullptr); // Checking this step is necessary before using a pointer to point to some content\n        auto p = dynamic_cast<PlaceHolder<T>*>(_content);\n        assert(p && "Bad any_cast"); // If the type conversion fails, p will be a nullptr; here we utilize the proximity principle of &&, the character conversion here must be non-zero\n        return &p->_val; // The priority of taking the address is lower than that of the arrow operator, and the final return is T*\n    }

For the nested classes, holder serves as the abstract base class, while placeholder serves as the class template.

Note
  • • dynamic_cast is provided by C++, a runtime type identification operator used for safely performing type conversions within class hierarchies. It has two main uses: downcasting from base class to derived class and cross-conversion in multiple inheritance, i.e., cross-conversion between base class pointers of different inheritance branches. Additionally, note that dynamic_cast can only be used for pointers and references of classes containing virtual functions (polymorphic types). A failed pointer conversion returns nullptr, while a failed reference conversion throws an exception.
  • • The priority of taking the address is lower than that of the arrow operator.
  • • The type returned by typeid() is const std::type_info&, so the return value of the Type function in the Any class should correspond to this. It took a long time to debug this, and the error reported a lot: for unfamiliar functions, special attention should be paid to their return types, parameter types, etc., and assumptions should not be made.
  • • The noexcept keyword is used to inform the compiler that the function will not throw exceptions.
  • • When the current class is used as a base class and needs to use a pointer or reference to the base class to call the derived class, its destructor must be declared as virtual; otherwise, when releasing an object using a pointer or reference to the base class, only the base class part of the object will be destructed, leading to memory leak issues. A common concise way to write this is virtual ~Holder() = default;

2.23 The Four Move and Copy Functions of the Any Class

Finally, there are copy constructor, copy assignment, move constructor, and move assignment. Move construction was added in C++11, which optimizes performance by “stealing” rvalues (which have no named storage address and can only be on the right side of the assignment) to reduce the overhead of copying.

    // Copy constructor and copy assignment\n    Any(const Any& other):_content(other._content?other._content->clone():nullptr){};\n    // Swap auxiliary assignment operator\n    Any& swap(Any& other) noexcept {\n        std::swap(_content, other._content);\n        return *this;\n    }\n    template<class T>\n    Any& operator=(const T& val) {\n        Any tmp(val); // Modern syntax, no need to manually delete the object, but note that it cannot be written as this->swap(Any(val)); here Any(val) is a temporary rvalue object, and after its lifecycle, this line will be executed, while swap needs to take a lvalue\n        swap(tmp);\n        return *this;\n    }\n    Any& operator=(const Any& other) {\n        if(&other != this) // Copy assignment easily forgets to check if it is copying itself; if the user makes an error, it will cause memory leaks\n        {\n            Any tmp(other);\n            this->swap(tmp);\n        }\n        return *this;\n    }\n    // Move constructor and move assignment\n    Any(Any&& other) noexcept :_content(other._content) {\n        other._content = nullptr;\n    }\n    Any& operator=(Any&& other) noexcept {\n        if(&other != this) {\n            delete _content;\n            _content = other._content;\n            other._content = nullptr;\n        }\n        return *this;\n    }
Note
  • • The modern syntax for the copy constructor cannot be written as this->swap(Any(val)); here Any(val) is a temporary rvalue object, and after its lifecycle, this line will be executed, while swap needs to take a lvalue.
  • • Copy assignment and move assignment easily forget to check if it is copying itself.
  • • The code for move construction and assignment is worth reviewing multiple times.

3. Insights

The first feeling of learning and using the Any class is, “It feels so similar!” It is very much like everything being an object in Python, where a variable can declare different data types, and the storage of data types is not limited to changing just once; it can store an int and then a string, which might be the principle of encapsulation in Python. However, after researching, it turns out that it is not. A vivid explanation is that the C++ Any class is like a box that holds variables, while variables in Python are actually name bindings that bind variable names to objects. When there is no variable name binding to an object, the object is subject to garbage collection.

Additionally, when I first learned about C++ object mechanisms, I did not quite understand why, after defining a derived class, I had to use a base class pointer to call it. The general answer given in books is to achieve polymorphism. The Any class provides a scenario to explain this issue. It means that through a holder pointer, multiple types of placeholder objects can be called, and importantly, it allows for easy unified management of multiple types of already created or uncreated subclass objects, i.e., polymorphism.

The Any class is an excellent container for reviewing C++11 features and practicing object polymorphism.

4. Source Code

// Implement an Any class to solve the multi-protocol problem on the server\nclass Any {\npublic: \n    Any() : _content(nullptr) {};\n    ~Any() {\n        if (_content != nullptr) // Before releasing a resource, always consider whether there is a resource to release\n            delete _content;\n    }\n    // Universal constructor\n    template<class T> // decay_t<T> is a method provided by C++ to downgrade the type, ensuring that Any stores the correct type\n    Any(T&& val) : _content(new PlaceHolder<std::decay_t<T>>(std::forward<T>(val))) {};\n    // Returns a pointer to the data stored in the subclass object\n    template<class T>\n    T* Get() {\n        assert(_content != nullptr); // Checking this step is necessary before using a pointer to point to some content\n        auto p = dynamic_cast<PlaceHolder<T>*>(_content);\n        assert(p && "Bad any_cast"); // If the type conversion fails, p will be a nullptr; here we utilize the proximity principle of &&, the character conversion here must be non-zero\n        return &p->_val; // The priority of taking the address is lower than that of the arrow operator, and the final return is T*\n    }\n    const std::type_info& Type() noexcept { return _content->Type(); }\n    // Copy constructor and copy assignment\n    Any(const Any& other) : _content(other._content ? other._content->clone() : nullptr) {};\n    template<class T>\n    Any& operator=(const T& val) {\n        Any tmp(val);\n        swap(tmp);\n        return *this;\n    }\n    Any& operator=(const Any& other) {\n        if (&other != this) {\n            Any tmp(other);\n            this->swap(tmp);\n        }\n        return *this;\n    }\n    // Move constructor and move assignment\n    Any(Any&& other) noexcept : _content(other._content) {\n        other._content = nullptr;\n    }\n    Any& operator=(Any&& other) noexcept {\n        if (&other != this) {\n            delete _content;\n            _content = other._content;\n            other._content = nullptr;\n        }\n        return *this;\n    }\nprivate: \n    // Swap auxiliary assignment operator\n    Any& swap(Any& other) noexcept {\n        std::swap(_content, other._content);\n        return *this;\n    }\n    class Holder {\n    public: \n        virtual const std::type_info& Type() const = 0;\n        virtual Holder* clone() = 0;\n        virtual ~Holder() = default;\n    };\n    template<class T>\n    class PlaceHolder : public Holder { // The class that actually stores the data\n    public: \n        PlaceHolder(const T& val) : _val(val) {};\n        // Get the data stored in the subclass object\n        virtual const std::type_info& Type() const { return typeid(_val); }\n        // Clone the subclass object\n        virtual Holder* clone() { return new PlaceHolder(_val); } // Provides a method to externally copy the current data for use with the copy constructor\n        T _val;\n    };\nprivate: \n    Holder* _content;\n};

Leave a Comment