Introduction
I have a display module:
There is a 128 * 64 monochrome on the module Display , A single-chip computer (b) controls the content it displays. The I? C bus of single chip microcomputer is connected to other single chip microcomputer (A) through the row pin bus on four sides, A sends instructions to B, and B draws.
B can send the display data to the screen byte by byte, but it can't be read, so the program must set the display memory. A frame needs 1024 bytes, but SCM B only has 512 bytes of memory, of which only 256 bytes can be allocated as display memory. The solution to this problem is to divide the display screen into four areas in the B program, save all the information of the graphics to be drawn, draw 1 / 4 screen in 256 bytes each time, draw and send in batches.
In short, I need to maintain multiple types of data. To be a little more specific, I'll put them in an array like structure, then iterate through the array and draw every element.
Different graphics are treated in the same way, which is the best practice of inheritance and polymorphism. I can design a Shape class and define virtual void draw() const = 0; every time I receive an instruction, I will get a new Line, Rectangle and other types of objects, put them into STD:: vector < Shape * > and call - > draw() for each Shape * pointer in traversal.
But I'm sorry. I'm on the new line today. The single-chip program pays attention to the running efficiency, except for the initialization, it's better not to blind new. I'm afraid it's not appropriate to delete each instruction with new and clear screen instructions!
I need value polymorphism, a polymorphism that can be represented by the object itself without a pointer or reference.
background
I have to introduce some knowledge first. What the novice who just finished the introductory course of C + + can't understand is the knowledge necessary to go deep into the bottom layer of C + + and experience the design idea of C + +. Because of these knowledge, I can come up with "value polymorphism" and realize it. If you are familiar with this knowledge, or can't wait to know how I realize value polymorphism, you can directly pull it down to the following Realization One section.
polymorphic
Polymorphism refers to providing a unified interface for different types of entities, or using the same symbol to represent different types. There are many polymorphisms in C + +:
First, compile time polymorphism. A non template function overload is a polymorphism. Functions called with the same name may be different, depending on the type of parameter. If you need a function name to handle more than one type, you have to write an overload. This polymorphism is a closed polymorphism. The good thing is that the new overload is not written with the original function.
A template is an open polymorphism - adapting a new type requires that new type, and the template is unchanged. Compared with the runtime polymorphism in the following article, the "t" of "STL" is enough to show that C + + encourages templates. Look, the standard library's algorithms are all template functions, rather than letting iterators inherit from the iterator < T > base class as in design pattern.
The disadvantage of template polymorphism lies in that the object of template parameter type T must be ready to use, and the function will not be maintained for a long time after it is returned. Use type erase if necessary.
Runtime polymorphism can be roughly divided into inheritance and type erasure, which are both open polymorphism. Inheritance and virtual function, also known as OOP, I call them "traditional polymorphism" in the title of this article, which I think is no objection. The four characteristics of object-oriented programming language, abstraction, encapsulation, inheritance and polymorphism, are all familiar to us (sometimes without abstraction), so that some people say polymorphism is virtual function. It's true that inheritance is widely used in many programs, but since function/bind has been "redeemed", we should learn from them, use them, and learn their design and ideas, and replace inheritance as a set of tools within a reasonable range, because they do have many problems - "Batman is a bird and a beast, and water plane can fly and swim", multiple inheritance, virtual inheritance, and various overhead ... Even Lippman couldn't look down:
Another major problem of inheritance is that polymorphism needs a layer of indirection, that is, pointer or reference. Take the iterator as an example. If the begin method returns a pointer to the iterator < T > object coming out of the new news, the customer must remember to delete the iterator after using it, or use the general RAII class of STD:: lock'guard to be responsible for the deletion of the iterator. In a word, it needs more work.
Therefore, in modern C + +, polymorphism based on type erasure gradually takes the upper hand. Type erasure uses a class to wrap multiple objects with similar interfaces. It belongs to the polymorphic wrapper in function. For example, std::function is a polymorphic function wrapper. The polymorphic value originally planned to be standardized in C++20 is a polymorphic value wrapper - very close to my intention. These will be discussed in detail later.
Personally, these two runtime polymorphisms are only semantically different.
The realization of virtual function
The most attractive part of deep exploration c + + object model is the realization of virtual function. Although the C + + standard does not make any rules and assumptions about the implementation of virtual functions, it is a secret in this small circle to realize polymorphism with a pointer to the virtual function table.
Suppose there are two classes:
class Base { public: Base(int i) : i(i) { } virtual ~Base() { } virtual void func() const { std::cout << "Base: " << i << std::endl; } private: int i; }; class Derived : public Base { public: Derived(int i, int j) : Base(i), j(j) { } virtual ~Derived() { } virtual void func() const override { std::cout << "Derived: " << j << std::endl; } private: int j; };
The layout of the instances of these two classes in memory may be as follows:
If you assign a pointer to a Derived instance to a variable of Base*, and then call func(), the program will take the object that the pointer points to as an instance of Base, extract its second lattice, find the function pointer of func in vtable's subscript 2, and then pass this pointer to it. Although it is regarded as a base instance, the vtable of this object actually points to the vtable of the derived class, so the called function is Derived::func, which is how inheritance based polymorphism is implemented.
If you assign a Derived instance to the Base variable, only i will be copied, vtable will be initialized to the vtable of Base, and j will be lost. The func that calls it, Base::func, will execute, and is likely to be called directly rather than through a function pointer.
This implementation can be extended to the case of the inheritance tree (emphasizing "tree", that is, single inheritance). As for pointer offset in multiple inheritance and sub object pointer in virtual inheritance, it is too complex, I will not introduce it.
vtable pointer does not copy is the culprit of virtual function pointer semantics, but it is also a must. Copying vtable pointer will cause more trouble: if there is a Derived virtual function table pointer in the Base instance, calling func will access the third cell of the object, but the third cell is invalid memory space. In contrast, it is a better choice to leave the task of maintaining pointers to programmers.
Type Erasure
Without copying vtable, value semantics cannot be implemented. Copying vtable will cause access problems. What causes this problem? Because the Base and Derived instances are different sizes. The class that implements type erasure also uses the same or similar polymorphic implementation as vtable. As one class rather than multiple classes, the size of type erasure class is determined. Therefore, vtable or its analogues can be copied and the value semantics can be realized. C + + tries to make class types behave like built-in types, which is the deeper meaning of type erasure.
Type erasure, as the name implies, is to erase the type of an object so that you can perform some operations on it without knowing its type. For example, std::function has a template constructor with constraints. You can use it to wrap any callable object with parameter type matching. After the constructor ends, not only you, but std::function does not know what type of instance it wraps, but operator() can call that callable object. I dissected it in an article Implementation of std::function Of course, there are many ways to implement it, and the implementation of other types of erasure classes are the same. They all contain two elements: a template constructor with possible constraints, and a function pointer, whether visible (direct maintenance) or invisible (using inheritance).
To get a more realistic feeling, let's write a simple type of erasure:
class MyFunction { private: class FunctorWrapper { public: virtual ~FunctorWrapper() = default; virtual FunctorWrapper* clone() const = 0; virtual void call() const = 0; }; template<typename T> class ConcreteWrapper : public FunctorWrapper { public: ConcreteWrapper(const T& functor) : functor(functor) { } virtual ~ConcreteWrapper() override = default; virtual ConcreteWrapper* clone() const { return new ConcreteWrapper(*this); } virtual void call() const override { functor(); } private: T functor; }; public: MyFunction() = default; template<typename T> MyFunction(T&& functor) : ptr(new ConcreteWrapper<T>(functor)) { } MyFunction(const MyFunction& other) : ptr(other.ptr->clone()) { } MyFunction& operator=(const MyFunction& other) { if (this != &other) { delete ptr; ptr = other.ptr->clone(); } return *this; } MyFunction(MyFunction&& other) noexcept : ptr(std::exchange(other.ptr, nullptr)) { } MyFunction& operator=(MyFunction&& other) noexcept { if (this != &other) { delete ptr; ptr = std::exchange(other.ptr, nullptr); } return *this; } ~MyFunction() { delete ptr; } void operator()() const { if (ptr) ptr->call(); } FunctorWrapper* ptr = nullptr; };
A function wrapper pointer is maintained in MyFunction class, which points to a concretewrapper < T > instance and calls virtual function to realize polymorphism. Virtual functions include destruct, clone and call, which are used for destruct, copy and function call of MyFunction respectively.
There is always a bit of type information in the implementation of type erasure class. The type information about T in MyFunction class is shown in vtable of FunctorWrapper, which is essentially function pointer. The type erasure class can also skip the inheritance tool and directly use function pointer to realize polymorphism. No matter which implementation is used, the type erasure class can always be copied or moved or both, and polymorphism can be embodied by the object itself.
Not every drop of milk is called terensus, and not every instance of a class can be wrapped by MyFunction. MyFunction requires t to be copied and can be called with operator()() const, which are called "affordance" of type T. When it comes to AF force, ordinary template functions also have AF force force for template types. For example, std::sort requires that iterators can be accessed randomly, otherwise the compiler will give you a lot of lengthy error information. C++20 introduces the concept and requires clauses, which are good for compiler and programmer.
Afforce force of each type of erasure class is determined at the time of writing. Afforce force is required not to inherit a base class, but only to see if you have a corresponding method for this class, just like Python, as long as the function interface matches. This type of recognition is called "duck typing", which comes from "duck test", which means "if it looks like a duck, swims like a duck, and quacks like a duck, then it probability is a duck".
The afforce force required by type erasure class is usually unary, that is, the parameter of member function does not contain T, for example, for the class wrapping integers, you can require T + 42, but you cannot require T + U. an instance of type erasure class does not know the information of another instance belonging to the same class but constructed from different types of objects. I think there is an exception to this rule. operator = = can be supported by some means.
Although MyFunction class implements value polymorphism, it still uses new and delete statements. If the callable object is just a simple function pointer, is it necessary to open space on the heap?
SBO
Small objects are stored in class instances, and large objects are handed over to the heap and pointers are maintained in the instances. This technique is called small buffer optimization (SBO). Most types of erasure classes should use SBO to save memory and improve efficiency. The problem is that SBO does not coexist with inheritance. It is troublesome to maintain a vtable or several function pointers in each instance, and also slows down compilation speed.
But in the face of memory and performance, can this work?
class MyFunction { private: static constexpr std::size_t size = 16; static_assert(size >= sizeof(void*), ""); struct Data { Data() = default; char dont_use[size]; } data; template<typename T> static void functorConstruct(Data& dst, T&& src) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) new ((U*)&dst) U(std::forward<U>(src)); else *(U**)&dst = new U(std::forward<U>(src)); } template<typename T> static void functorDestructor(Data& data) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) ((U*)&data)->~U(); else delete *(U**)&data; } template<typename T> static void functorCopyCtor(Data& dst, const Data& src) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) new ((U*)&dst) U(*(const U*)&src); else *(U**)&dst = new U(**(const U**)&src); } template<typename T> static void functorMoveCtor(Data& dst, Data& src) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) new ((U*)&dst) U(*(const U*)&src); else *(U**)&dst = std::exchange(*(U**)&src, nullptr); } template<typename T> static void functorInvoke(const Data& data) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) (*(U*)&data)(); else (**(U**)&data)(); } template<typename T> static void (*const vtables[4])(); void (*const* vtable)() = nullptr; public: MyFunction() = default; template<typename T> MyFunction(T&& obj) : vtable(vtables<T>) { functorConstruct(data, std::forward<T>(obj)); } MyFunction(const MyFunction& other) : vtable(other.vtable) { if (vtable) ((void (*)(Data&, const Data&))vtable[1])(this->data, other.data); } MyFunction& operator=(const MyFunction& other) { this->~MyFunction(); vtable = other.vtable; new (this) MyFunction(other); return *this; } MyFunction(MyFunction&& other) noexcept : vtable(std::exchange(other.vtable, nullptr)) { if (vtable) ((void (*)(Data&, Data&))vtable[2])(this->data, other.data); } MyFunction& operator=(MyFunction&& other) noexcept { this->~MyFunction(); new (this) MyFunction(std::move(other)); return *this; } ~MyFunction() { if (vtable) ((void (*)(Data&))vtable[0])(data); } void operator()() const { if (vtable) ((void (*)(const Data&))vtable[3])(this->data); } }; template<typename T> void (*const MyFunction::vtables[4])() = { (void (*)())MyFunction::functorDestructor<T>, (void (*)())MyFunction::functorCopyCtor<T>, (void (*)())MyFunction::functorMoveCtor<T>, (void (*)())MyFunction::functorInvoke<T>, };
(if you can fully understand this code, it shows that your C language skills are very solid! If you don't understand, Realization There is a more readable version in.)
Now the MyFunction class acts as the original FunctorWrapper, using vtable to implement polymorphism. Whenever MyFunction instance is assigned a callable object, vtable is initialized as a pointer to VTables < T >, which is used for vtable of type T (here, the variable template of C++14 is used). vtable contains four function pointers, which are used to parse, copy, move and call the T instance.
Taking the destructor functordestructor < T > as an example, u is the type of T after STD:: decode, which is used to handle the conversion of functions to function pointers. MyFunction class defines size byte spatial data, which is used to store one of the pointers of small callable objects or large callable objects. Functordestructor < T > knows the specific situation: when sizeof (U) < = size, data stores the callable object itself, interprets data as u and calls its destructor ~ U(); when sizeof (U) > Size, data stores the pointer, interprets data as u * and delete s it. The principle of other functions is the same. Note that new ((U *) & DST) U (STD:: forward < U > (SRC)); is to locate the new statement.
Except for the constructor with parameter T, other member functions of MyFunction call T's methods through vtable, because none of them know what T is. During copying, unlike the instance of the FunctorWrapper subclass, the vtable of MyFunction is copied together, and the value polymorphism is still implemented - some new is avoided, which is in line with my intention. But it's not over.
polymorphic_value
polymorphic_value It is a class template that has realized value polymorphism. It was originally set to be standardized in C++20, but it is not included in C++20, and it is expected to enter C++23 standard (at that time, I didn't have to write or not). Up to now, my understanding of the source code of polymorphic Φ value is still in a state of half understanding. I can only briefly introduce it.
The template parameter t of polymorphic ũ U value is a class type. Any subclass U of T and T, and any instance of polymorphic ũ U value < U > can be used to construct the polymorphic ũ U value object. Polymerphic UU value object can be copied, and its value can also be copied, and const (const T &, obtained through const polymerphic UU value), which makes it different from unique_ptr and shared_ptr; polymerphic UU value is different from type erasure, because it respects inheritance and does not use duck typing.
However, an SBO added issue , no one has replied all the time -- this reflects that the implementation of polymorphoc ﹣ value is not simple -- in the current version, no matter the size of the object, the polymorphoc ﹣ value will always come out with a new control ﹣ block; for instances constructed from a different type of polymorphoc ﹣ value, there will also be a delegating ﹣ control ﹣ block, which has great performance at runtime Influence. In my opinion, SBO can solve both problems, which also reflects the problems of inheritance tools.
Interface
I want to implement three classes: Shape, the base class with multiple values; Line, containing four integers as coordinates, to demonstrate the first case of SBO; Rectangle, containing four integers and a bool value, which indicates whether the Rectangle is filled, to demonstrate the second case. They behave like classes in STL, with default constructors, destructors, copies, mobile constructs and assignments, swap, and support for operator = = and draw. Operator = = returns false when the two parameter types are different, and compares their contents when they are the same; draw is a polymorphic function that outputs the information of the graph in the demo program.
A simple implementation is to use std::function plus adapter:
#include <iostream> #include <functional> #include <new> struct Point { int x; int y; }; std::ostream& operator<<(std::ostream& os, const Point& point) { os << point.x << ", " << point.y; return os; } class Shape { private: template<typename T> class Adapter { public: Adapter(const T& shape) : shape(shape) { } void operator()() const { shape.draw(); } private: T shape; }; public: template<typename T> Shape(const T& shape) : function(Adapter<T>(shape)) { } void draw() const { function(); } private: std::function<void()> function; }; class Line { public: Line() { } Line(Point p0, Point p1) : endpoint{ p0, p1 } { } Line(const Line&) = default; Line& operator=(const Line&) = default; void draw() const { std::cout << "Drawing a line: " << endpoint[0] << "; " << endpoint[1] << std::endl; } private: Point endpoint[2]; }; class Rectangle { public: Rectangle() { } Rectangle(Point v0, Point v1, bool filled) : vertex{ v0, v1 }, filled(filled) { } Rectangle(const Rectangle&) = default; Rectangle& operator=(const Rectangle&) = default; void draw() const { std::cout << "Drawing a rectangle: " << vertex[0] << "; " << vertex[1] << "; " << (filled ? "filled" : "blank") << std::endl; } private: Point vertex[2]; bool filled; };
The following implementation is the same as this code, but more "pure".
Realization
#include <iostream> #include <new> #include <type_traits> #include <utility> struct Point { int x; int y; bool operator==(const Point& rhs) const { return this->x == rhs.x && this->y == rhs.y; } }; std::ostream& operator<<(std::ostream& os, const Point& point) { os << point.x << ", " << point.y; return os; } class Shape { protected: using FuncPtr = void (*)(); using FuncPtrCopy = void (*)(Shape*, const Shape*); static constexpr std::size_t funcIndexCopy = 0; using FuncPtrDestruct = void (*)(Shape*); static constexpr std::size_t funcIndexDestruct = 1; using FuncPtrCompare = bool (*)(const Shape*, const Shape*); static constexpr std::size_t funcIndexCompare = 2; using FuncPtrDraw = void (*)(const Shape*); static constexpr std::size_t funcIndexDraw = 3; static constexpr std::size_t funcIndexTotal = 4; class ShapeData { public: static constexpr std::size_t size = 16; template<typename T> struct IsLocal : std::integral_constant<bool, (sizeof(T) <= size) && std::is_trivially_copyable<T>::value> { }; private: char placeholder[size]; template<typename T, typename U = void> using EnableIfLocal = typename std::enable_if<IsLocal<T>::value, U>::type; template<typename T, typename U = void> using EnableIfHeap = typename std::enable_if<!IsLocal<T>::value, U>::type; public: ShapeData() { } template<typename T, typename... Args> EnableIfLocal<T> construct(Args&& ... args) { new (reinterpret_cast<T*>(this)) T(std::forward<Args>(args)...); } template<typename T, typename... Args> EnableIfHeap<T> construct(Args&& ... args) { this->access<T*>() = new T(std::forward<Args>(args)...); } template<typename T> EnableIfLocal<T> destruct() { this->access<T>().~T(); } template<typename T> EnableIfHeap<T> destruct() { delete this->access<T*>(); } template<typename T> EnableIfLocal<T, T&> access() { return reinterpret_cast<T&>(*this); } template<typename T> EnableIfHeap<T, T&> access() { return *this->access<T*>(); } template<typename T> const T& access() const { return const_cast<ShapeData*>(this)->access<T>(); } }; Shape(const FuncPtr* vtable) : vtable(vtable) { } public: Shape() { } Shape(const Shape& other) : vtable(other.vtable) { if (vtable) reinterpret_cast<FuncPtrCopy>(vtable[funcIndexCopy])(this, &other); } Shape& operator=(const Shape& other) { if (this != &other) { if (vtable) reinterpret_cast<FuncPtrDestruct>(vtable[funcIndexDestruct]) (this); vtable = other.vtable; if (vtable) reinterpret_cast<FuncPtrCopy>(vtable[funcIndexCopy]) (this, &other); } return *this; } Shape(Shape&& other) noexcept : vtable(other.vtable), data(other.data) { other.vtable = nullptr; } Shape& operator=(Shape&& other) noexcept { swap(other); return *this; } ~Shape() { if (vtable) reinterpret_cast<FuncPtrDestruct>(vtable[funcIndexDestruct])(this); } void swap(Shape& other) noexcept { using std::swap; swap(this->vtable, other.vtable); swap(this->data, other.data); } bool operator==(const Shape& rhs) const { if (this->vtable == nullptr || this->vtable != rhs.vtable) return false; return reinterpret_cast<FuncPtrCompare>(vtable[funcIndexCompare]) (this, &rhs); } bool operator!=(const Shape& rhs) const { return !(*this == rhs); } void draw() const { if (vtable) reinterpret_cast<FuncPtrDraw>(vtable[funcIndexDraw])(this); } protected: const FuncPtr* vtable = nullptr; ShapeData data; template<typename T> static void defaultCopy(Shape* dst, const Shape* src) { dst->data.construct<T>(src->data.access<T>()); } template<typename T> static void defaultDestruct(Shape* shape) { shape->data.destruct<T>(); } template<typename T> static bool defaultCompare(const Shape* lhs, const Shape* rhs) { return lhs->data.access<T>() == rhs->data.access<T>(); } }; namespace std { void swap(Shape& lhs, Shape& rhs) noexcept { lhs.swap(rhs); } } class Line : public Shape { private: struct LineData { Point endpoint[2]; LineData() { } LineData(Point p0, Point p1) : endpoint{ p0, p1 } { } bool operator==(const LineData& rhs) const { return this->endpoint[0] == rhs.endpoint[0] && this->endpoint[1] == rhs.endpoint[1]; } bool operator!=(const LineData& rhs) const { return !(*this == rhs); } }; static_assert(ShapeData::IsLocal<LineData>::value, ""); public: Line() : Shape(lineVtable) { data.construct<LineData>(); } Line(Point p0, Point p1) : Shape(lineVtable) { data.construct<LineData>(p0, p1); } Line(const Line&) = default; Line& operator=(const Line&) = default; Line(Line&&) = default; Line& operator=(Line&&) = default; ~Line() = default; private: static const FuncPtr lineVtable[funcIndexTotal]; static ShapeData& accessData(Shape* shape) { return static_cast<Line*>(shape)->data; } static const ShapeData& accessData(const Shape* shape) { return accessData(const_cast<Shape*>(shape)); } static void lineDraw(const Shape* line) { auto& data = static_cast<const Line*>(line)->data.access<LineData>(); std::cout << "Drawing a line: " << data.endpoint[0] << "; " << data.endpoint[1] << std::endl; } }; const Shape::FuncPtr Line::lineVtable[] = { reinterpret_cast<Shape::FuncPtr>(Shape::defaultCopy<LineData>), reinterpret_cast<Shape::FuncPtr>(Shape::defaultDestruct<LineData>), reinterpret_cast<Shape::FuncPtr>(Shape::defaultCompare<LineData>), reinterpret_cast<Shape::FuncPtr>(Line::lineDraw), }; class Rectangle : public Shape { private: struct RectangleData { Point vertex[2]; bool filled; RectangleData() { } RectangleData(Point v0, Point v1, bool filled) : vertex{ v0, v1 }, filled(filled) { } bool operator==(const RectangleData& rhs) const { return this->vertex[0] == rhs.vertex[0] && this->vertex[1] == rhs.vertex[1] && this->filled == rhs.filled; } bool operator!=(const RectangleData& rhs) const { return !(*this == rhs); } }; static_assert(!ShapeData::IsLocal<RectangleData>::value, ""); public: Rectangle() : Shape(rectangleVtable) { data.construct<RectangleData>(); } Rectangle(Point v0, Point v1, bool filled) : Shape(rectangleVtable) { data.construct<RectangleData>(v0, v1, filled); } Rectangle(const Rectangle&) = default; Rectangle& operator=(const Rectangle&) = default; Rectangle(Rectangle&&) = default; Rectangle& operator=(Rectangle&&) = default; ~Rectangle() = default; private: static const FuncPtr rectangleVtable[funcIndexTotal]; static ShapeData& accessData(Shape* shape) { return static_cast<Rectangle*>(shape)->data; } static const ShapeData& accessData(const Shape* shape) { return accessData(const_cast<Shape*>(shape)); } static void rectangleDraw(const Shape* rect) { auto& data = accessData(rect).access<RectangleData>(); std::cout << "Drawing a rectangle: " << data.vertex[0] << "; " << data.vertex[1] << "; " << (data.filled ? "filled" : "blank") << std::endl; } }; const Shape::FuncPtr Rectangle::rectangleVtable[] = { reinterpret_cast<Shape::FuncPtr>(Shape::defaultCopy<RectangleData>), reinterpret_cast<Shape::FuncPtr>(Shape::defaultDestruct<RectangleData>), reinterpret_cast<Shape::FuncPtr>(Shape::defaultCompare<RectangleData>), reinterpret_cast<Shape::FuncPtr>(Rectangle::rectangleDraw), }; template<typename T> Shape test(const T& s0) { s0.draw(); T s1 = s0; s1.draw(); T s2; s2 = s1; s2.draw(); Shape s3 = s0; s3.draw(); Shape s4; s4 = s0; s4.draw(); Shape s5 = std::move(s0); s5.draw(); Shape s6; s6 = std::move(s5); s6.draw(); return s6; } int main() { Line line({ 1, 2 }, { 3, 4 }); auto l2 = test(line); Rectangle rect({ 5, 6 }, { 7, 8 }, true); auto r2 = test(rect); std::swap(l2, r2); l2.draw(); r2.draw(); }
object model
As mentioned before, the essence of traditional polymorphism and type erasure is the same. They all use function pointers and put them in vtable or object. In the inheritance system of Shape, Line and Rectangle are concrete classes. It is very easy to write two VTables, so I adopted the implementation of vtable.
Line and Rectangle inherit from shape. In order not to be clipped when copying values, the memory layout of the three classes must be the same. That is to say, line and Rectangle cannot define new data members. Shape reserves 16 bytes of space for subclasses to store line data or pointers to Rectangle data, the latter is specially arranged for demonstration by me (the two static "assert" is just to ensure that the demonstration is in place, not my assumptions about the memory layout of the two subclasses).
SBO type
ShapeData is the data space in a Shape. The stored value or pointer is determined by ShapeData and data type together. If the determined task is handed over to a specific data type, ShapeData is difficult to modify the size. Therefore, I design ShapeData as a type with template function, and take the data type as the template parameter T to provide the operation of construction, analysis and access. There are two versions each , which specific call can be decided by the compiler, so as to improve the maintainability of the program.
std::function also uses SBO Read its source code I found that the boundary between the two cases can not only be the size of the data type, but also is "tiny" copyable and so on. The advantage of this is that mobile and swap can use near default behavior.
class ShapeData { public: static constexpr std::size_t size = 16; static_assert(size >= sizeof(void*), ""); template<typename T> struct IsLocal : std::integral_constant<bool, (sizeof(T) <= size) && std::is_trivially_copyable<T>::value> { }; private: char placeholder[size]; template<typename T, typename U = void> using EnableIfLocal = typename std::enable_if<IsLocal<T>::value, U>::type; template<typename T, typename U = void> using EnableIfHeap = typename std::enable_if<!IsLocal<T>::value, U>::type; public: ShapeData() { } template<typename T, typename... Args> EnableIfLocal<T> construct(Args&& ... args) { new (reinterpret_cast<T*>(this)) T(std::forward<Args>(args)...); } template<typename T, typename... Args> EnableIfHeap<T> construct(Args&& ... args) { this->access<T*>() = new T(std::forward<Args>(args)...); } template<typename T> EnableIfLocal<T> destruct() { this->access<T>().~T(); } template<typename T> EnableIfHeap<T> destruct() { delete this->access<T*>(); } template<typename T> EnableIfLocal<T, T&> access() { return reinterpret_cast<T&>(*this); } template<typename T> EnableIfHeap<T, T&> access() { return *this->access<T*>(); } template<typename T> const T& access() const { return const_cast<ShapeData*>(this)->access<T>(); } };
EnableIfLocal and EnableIfHeap are used SFNIAE Skills ( Here There is a similar example). I'm used to using SFINAE and tag dispatch if you like.
Virtual function table
C99 standard6.3.2.3 clause 8:
A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.
The implication is that all function pointers are the same size. There is no such stipulation in C + + standard, but I make this assumption (member function pointer is not included). As far as I know, this assumption is true in all mainstream platforms. Therefore, I define the type using FuncPtr = void (*) ();, and use the FuncPtr array as vtable to store function pointers of any type.
There are four function pointers in vtable, which are responsible for copying (not moving), destructing, comparing (operator = =), and draw ing. Function pointers are of different types, but they are not related to subclasses. They can be defined in Shape to simplify the following code. Obviously, the subscript of each function pointer cannot use magic number s such as 0, 1, 2, etc., and constants are defined in Shape for easy maintenance. Similar to the default keyword, Shape provides the default implementation of the first three functions, most of which do not need to be written separately.
class Shape { protected: using FuncPtr = void (*)(); using FuncPtrCopy = void (*)(Shape*, const Shape*); static constexpr std::size_t funcIndexCopy = 0; using FuncPtrDestruct = void (*)(Shape*); static constexpr std::size_t funcIndexDestruct = 1; using FuncPtrCompare = bool (*)(const Shape*, const Shape*); static constexpr std::size_t funcIndexCompare = 2; using FuncPtrDraw = void (*)(const Shape*); static constexpr std::size_t funcIndexDraw = 3; static constexpr std::size_t funcIndexTotal = 4; // ... public: // ... protected: const FuncPtr* vtable = nullptr; ShapeData data; template<typename T> static void defaultCopy(Shape* dst, const Shape* src) { dst->data.construct<T>(src->data.access<T>()); } template<typename T> static void defaultDestruct(Shape* shape) { shape->data.destruct<T>(); } template<typename T> static bool defaultCompare(const Shape* lhs, const Shape* rhs) { return lhs->data.access<T>() == rhs->data.access<T>(); } };
Method adaptation
All polymorphic functions must perform operations by calling functions in the virtual function table, including destruct, copy construct, copy assignment (no move), operator = = and draw.
class Shape { protected: // ... Shape(const FuncPtr* vtable) : vtable(vtable) { } public: Shape() { } Shape(const Shape& other) : vtable(other.vtable) { if (vtable) reinterpret_cast<FuncPtrCopy>(vtable[funcIndexCopy])(this, &other); } Shape& operator=(const Shape& other) { if (this != &other) { if (vtable) reinterpret_cast<FuncPtrDestruct>(vtable[funcIndexDestruct]) (this); vtable = other.vtable; if (vtable) reinterpret_cast<FuncPtrCopy>(vtable[funcIndexCopy]) (this, &other); } return *this; } Shape(Shape&& other) noexcept : vtable(other.vtable), data(other.data) { other.vtable = nullptr; } Shape& operator=(Shape&& other) noexcept { swap(other); return *this; } ~Shape() { if (vtable) reinterpret_cast<FuncPtrDestruct>(vtable[funcIndexDestruct])(this); } void swap(Shape& other) noexcept { using std::swap; swap(this->vtable, other.vtable); swap(this->data, other.data); } bool operator==(const Shape& rhs) const { if (this->vtable == nullptr || this->vtable != rhs.vtable) return false; return reinterpret_cast<FuncPtrCompare>(vtable[funcIndexCompare]) (this, &rhs); } bool operator!=(const Shape& rhs) const { return !(*this == rhs); } void draw() const { if (vtable) reinterpret_cast<FuncPtrDraw>(vtable[funcIndexDraw])(this); } protected: // ... }; namespace std { void swap(Shape& lhs, Shape& rhs) noexcept { lhs.swap(rhs); } }
Copy constructor copies vtable and data, destructor destroys data, copy assignment function first destructs and then copies. operator = = first check whether the VTables of the two parameters are the same. Only if they are the same, can the two parameters be of the same type for subsequent comparison. draw calls the corresponding function in vtable. All methods will check whether vtable is nullptr first, because Shape is an abstract class role, a Shape object is empty, and no operation is performed.
The special ones are mobile and swap. Because ShapeData data stores data types or pointers that are "trivial ly" copied, it can be copied directly in swap. (swap can be defaulted in such a non trial situation. Isn't it good to give a whole operator to swap?)
Mobile assignment exchanges * this with other, and gives other the task of analyzing * this. Mobile construction is also equivalent to swap, but this - > VTable = = nullptr. Actually, I can write copy-and-swap:
Shape& operator=(Shape other) { swap(other); return *this; }
It is used to replace shape & operator = (const shape &) and shape & operator = (shape & &). Unfortunately, shape & operator = (shape) is not a special member function specified by C + +, and subclasses will not inherit its behavior.
Subclasses inherit all of the above functions. I really want to write final to prevent subclass overwriting, but these functions are not virtual functions in C + + syntax. So we get the copy structure and draw of virtual and realize the value polymorphism.
discuss
I turned to the C + + standard and found that there was no implementation details of the standard. On every page of Fang Zhengzheng, there were several words "undefined behavior". I couldn't sleep. After watching the words carefully in the middle of the night, I could see that the words were "trade off" all over the book. If we want to sum up value polymorphism in one sentence, it is "more obligations, more rights".
security
The implementation code of Shape is full of mandatory type conversion, which is easy to cause the question of its type security. Because LineData and lineVtable are always bound together, virtual functions will not access data of non corresponding types. Even if there is an error at this point, the program will not crash as long as the data type is relatively trivial (excluding pointers and the like). However, the premise of type security is that the size of the base class and the derived class are the same. If the customer violates this, I have to use the traditional C/C + + skill - undefined behavior.
Type safety is not the same as "type right" - a name I pick up casually. In the above demo program, if I std::swap(line, rect), line will store a Rectangle instance, but line is a line instance in syntax! That is to say, line and Rectangle can only guarantee the correct type when defining variables. After that, they will pass the leave with Shape.
Type security guarantees that illegal address space will not be accessed. Does memory leak occur? It is impossible to construct according to the second case of SBO, new, and analyze according to the first case of trivially. The first premise is that the data type is paired with vtable, and on this basis, copy and destruct pair in vtable. It is more reassuring that which version of these functions is chosen at compile time.
There is also unusual security. As long as the client abides by some exception handling rules, so that the Shape's destructor can be called, it can ensure that no resources are not released.
performance
In space, value polymorphism inevitably wastes space. The reserved data area needs to be large enough to store most types of data. For the smaller one, a lot of space is wasted. For the larger one, only one pointer is wasted. Creatively, you can also put some of the trivial data locally and maintain a pointer for others, but that's too much trouble.
In time, the dynamic part of value polymorphism has better performance. Compared with type erasure based on inheritance, value polymorphism needs to maintain only one vtable pointer compared with type erasure of function pointer. Compared with virtual function, the original intention of value polymorphism is to avoid new and delete. However, the compiler is responsible for virtual functions. If the compiler has any trivial optimization, I admit defeat.
But the static part of value polymorphism is not satisfactory. In traditional polymorphism, if the type of a polymorphic instance can be determined at compile time, the virtual function will statically decide to call the function instead of vtable. In value polymorphism, a subclass can override the normal "virtual function" of the base class to improve the runtime performance. However, for copy control functions, no matter whether the subclass is overridden or not, the compiler will always call the corresponding function of the base class, and their task is polymorphic copy. Subclasses are unnecessary, sometimes they can't be overridden, let alone static resolution. But considering the situation that line is not line, it's better to be honest and practical.
There is room for trade-offs between time and space. In order to make the data of more subclasses be placed locally, the data space in the base class can be reserved larger, but more space will be wasted; the function pointer in vtable can be placed directly in the object, which takes up more space, in exchange for reducing the use of a solution every time; copy, analysis and comparison can be combined into a function to save space, but more is needed Parameters indicate which operation. In short, traditional arts can be implemented defined.
extend
I want to add a subclass of ThickLine to Line to represent a straight Line of a certain width. Drawing tilt curve on computer screen Bresenham algorithm , I'm not familiar with it. I hope the program can print some debugging information, so add a virtual function debug to Line (and Rectangle is easy to draw). Of course, it's not a virtual function in C + + syntax.
class Line : public Shape { protected: static constexpr std::size_t funcIndexDebug = funcIndexTotal; using FuncPtrDebug = void (*)(const Line*); static constexpr std::size_t funcIndexTotalLine = funcIndexTotal + 1; struct LineData { Point endpoint[2]; LineData() { } LineData(Point p0, Point p1) : endpoint{ p0, p1 } { } bool operator==(const LineData& rhs) const { return this->endpoint[0] == rhs.endpoint[0] && this->endpoint[1] == rhs.endpoint[1]; } bool operator!=(const LineData& rhs) const { return !(*this == rhs); } }; Line(const FuncPtr* vtable) : Shape(vtable) { } public: Line() : Shape(lineVtable) { data.construct<LineData>(); } Line(Point p0, Point p1) : Shape(lineVtable) { data.construct<LineData>(p0, p1); } Line(const Line&) = default; Line& operator=(const Line&) = default; Line(Line&&) = default; Line& operator=(Line&&) = default; ~Line() = default; void debug() const { if (vtable) reinterpret_cast<FuncPtrDebug>(vtable[funcIndexDebug])(this); } private: static const FuncPtr lineVtable[funcIndexTotalLine]; static ShapeData& accessData(Shape* shape) { return static_cast<Line*>(shape)->data; } static const ShapeData& accessData(const Shape* shape) { return accessData(const_cast<Shape*>(shape)); } static void lineDraw(const Shape* line) { auto& data = static_cast<const Line*>(line)->data.access<LineData>(); std::cout << "Drawing a line: " << data.endpoint[0] << "; " << data.endpoint[1] << std::endl; } static void lineDebug(const Line* line) { std::cout << "Line debug:\n\t"; lineDraw(line); } }; const Shape::FuncPtr Line::lineVtable[] = { reinterpret_cast<Shape::FuncPtr>(Shape::defaultCopy<LineData>), reinterpret_cast<Shape::FuncPtr>(Shape::defaultDestruct<LineData>), reinterpret_cast<Shape::FuncPtr>(Shape::defaultCompare<LineData>), reinterpret_cast<Shape::FuncPtr>(Line::lineDraw), reinterpret_cast<Shape::FuncPtr>(Line::lineDebug), }; class ThickLine : public Line { protected: struct ThickLineData { LineData lineData; int width; ThickLineData() { } ThickLineData(Point p0, Point p1, int width) : lineData{ p0, p1 }, width(width) { } ThickLineData(LineData data, int width) : lineData(data), width(width) { } bool operator==(const ThickLineData& rhs) const { return this->lineData == rhs.lineData && this->width == rhs.width; } bool operator!=(const ThickLineData& rhs) const { return !(*this == rhs); } }; public: ThickLine() : Line(thickLineVtable) { data.construct<ThickLineData>(); } ThickLine(Point p0, Point p1, int width) : Line(thickLineVtable) { data.construct<ThickLineData>(p0, p1, width); } ThickLine(const ThickLine&) = default; ThickLine& operator=(const ThickLine&) = default; ThickLine(ThickLine&&) = default; ThickLine& operator=(ThickLine&&) = default; ~ThickLine() = default; private: static const FuncPtr thickLineVtable[funcIndexTotalLine]; static ShapeData& accessData(Shape* shape) { return static_cast<ThickLine*>(shape)->data; } static const ShapeData& accessData(const Shape* shape) { return accessData(const_cast<Shape*>(shape)); } static void thickLineDraw(const Shape* line) { auto& data = static_cast<const ThickLine*>(line)->data.access<ThickLineData>(); std::cout << "Drawing a thick line: " << data.lineData.endpoint[0] << "; " << data.lineData.endpoint[1] << "; " << data.width << std::endl; } static void thickLineDebug(const Line* line) { std::cout << "ThickLine debug:\n\t"; thickLineDraw(line); } }; const Shape::FuncPtr ThickLine::thickLineVtable[] = { reinterpret_cast<Shape::FuncPtr>(Shape::defaultCopy<ThickLineData>), reinterpret_cast<Shape::FuncPtr>(Shape::defaultDestruct<ThickLineData>), reinterpret_cast<Shape::FuncPtr>(Shape::defaultCompare<ThickLineData>), reinterpret_cast<Shape::FuncPtr>(ThickLine::thickLineDraw), reinterpret_cast<Shape::FuncPtr>(ThickLine::thickLineDebug), };
It's more difficult to add data to the non abstract class Line than you think. The constructor of Line will construct SBO data segment as LineData, but what ThickLine needs is ThickLineData. It's not safe to construct ThickLine again on LineData, so I add a protected constructor to Line in imitation of Shape, and open LineData to ThickLine to define ThickLineData, which contains LineData.
This example shows that value polymorphism is not only suitable for a group of derived classes directly inheriting an abstract base class, but also can be extended to any inheritance chain / tree with single inheritance, including inheriting abstract classes and non abstract classes. The latter is a little more cumbersome. The base class needs to open the data type to the derived class, and let the derived class combine the base class data with the new data. This destroys the encapsulation of the base class to some extent. The solution is to define the method in the data type and let the value polymorphism class act as the adapter.
Single inheritance does not generalize all the "is-a" relationships. Sometimes multiple inheritance and virtual inheritance are necessary. Can value polymorphism support it? Answer: it is impossible, because the instance size of the derived class under multiple inheritance is larger than any base class, which conflicts with the requirement of value polymorphism that the memory layout of the base class and the derived class be consistent. This should be the most obvious limitation of value polymorphism.
Pattern
There is no way to force subclasses not to define data members, which leads to potential security problems. The compiler automatically calls the base class copy function to make the static resolution impossible. The derived class even destroys the encapsulation of the base class data. Is there a solution to these problems? In C language, similar problems are solved by cfont compiler. It is easy to think about whether value polymorphism can be a default polymorphism behavior of programming language. I think it's OK. It's especially suitable for small equipment, but some problems need to be considered.
It has just been proved that single inheritance is feasible while multiple inheritance is not. This programming language can only allow single inheritance. So between single inheritance and multi inheritance, multi inheritance, which removes the burden of data members, is similar to the interface in Java and C ා, feasible? I didn't think about it. I felt vaguely that there was a solution.
How much data space is reserved in the base class? If it is up to the programmer to decide, the programmer writes a number randomly, and the single-chip microcomputer has 8, 16, 32 bits, so that the code portability is reduced. Or it's up to the compiler, for example, to make 50% of the subclass data local. This may seem like harmony, but if you think about it, it's not friendly to linkers. Even worse, if there is such a definition:
class A { }; class B { }; class A1 : public A { B b; }; class B1 : public B { A a; };
To decide the size of A, we have to decide the size of B first; to decide the size of B, we have to decide the size of A first Well, we can work out an algorithm problem.
What do you want to do so much? It's like I've learned compilation principles.
Second to syntax, can value polymorphism be generalized into a general library? Polymorphoc_value is a ready-made but imperfect answer. Its main problem is that it can not directly construct polymorphoc_value < b > instance through polymorphoc_value < d > instance (where D is a derived class of B), which will lead to the time complexity of calling a method in extreme cases to \ (O(h) \) (where \ (h \) is the length of inheritance chain). There is also a small detail that naked value polymorphism is always better than any class library: you can write shape.draw() directly without shape - > draw(), which has some misleading semantics like a pointer. However, the polymorphic value supports multiple inheritance and virtual inheritance, which can never be compared with the value polymorphism.
I have been thinking for a long time, and I think that even if C + + research evolved into C + + +, there is no way that a class template can help the design of value polymorphism classes, only to use macros in the second place. The Shape family can be simplified as follows:
class Shape { VP_BASE(Shape, 16, 1); static constexpr std::size_t funcIndexDraw = 0; public: void draw() const { if (vtable) VP_BASE_VFUNCTION(void(*)(const Shape*), funcIndexDraw)(this); } }; VP_BASE_SWAP(Shape); class Line : public Shape { VP_DERIVED(Line); private: struct LineData { Point endpoint[2]; LineData() { } LineData(Point p0, Point p1) : endpoint{ p0, p1 } { } bool operator==(const LineData& rhs) const { return this->endpoint[0] == rhs.endpoint[0] && this->endpoint[1] == rhs.endpoint[1]; } bool operator!=(const LineData& rhs) const { return !(*this == rhs); } }; public: Line() : VP_DERIVED_INITIALIZE(Shape, Line) { VP_DERIVED_CONSTRUCT(LineData); } Line(Point p0, Point p1) : VP_DERIVED_INITIALIZE(Shape, Line) { VP_DERIVED_CONSTRUCT(LineData, p0, p1); } private: static void lineDraw(const Shape* line) { auto& data = VP_DERIVED_ACCESS(const Line, LineData, line); std::cout << "Drawing a line: " << data.endpoint[0] << "; " << data.endpoint[1] << std::endl; } }; VP_DERIVED_VTABLE(Line, LineData, VP_DERIVED_VFUNCTION(Line, lineDraw), ); class Rectangle : public Shape { VP_DERIVED(Rectangle); private: struct RectangleData { Point vertex[2]; bool filled; RectangleData() { } RectangleData(Point v0, Point v1, bool filled) : vertex{ v0, v1 }, filled(filled) { } bool operator==(const RectangleData& rhs) const { return this->vertex[0] == rhs.vertex[0] && this->vertex[1] == rhs.vertex[1] && this->filled == rhs.filled; } bool operator!=(const RectangleData& rhs) const { return !(*this == rhs); } }; public: Rectangle() : VP_DERIVED_INITIALIZE(Shape, Rectangle) { VP_DERIVED_CONSTRUCT(RectangleData); } Rectangle(Point v0, Point v1, bool filled) : VP_DERIVED_INITIALIZE(Shape, Rectangle) { VP_DERIVED_CONSTRUCT(RectangleData, v0, v1, filled); } private: static void rectangleDraw(const Shape* rect) { auto& data = VP_DERIVED_ACCESS(const Rectangle, RectangleData, rect); std::cout << "Drawing a rectangle: " << data.vertex[0] << "; " << data.vertex[1] << "; " << (data.filled ? "filled" : "blank") << std::endl; } }; VP_DERIVED_VTABLE(Rectangle, RectangleData, VP_DERIVED_VFUNCTION(Rectangle, rectangleDraw), );
The effect is general, not much simplified. Not only that, if you don't want your value polymorphism class to support operator = = you have to write a new macro, which is very rigid.
Again, can value polymorphism be a design pattern? I think it has the potential to be a design pattern, because each value polymorphism class has a similar memory layout, which can extract common code and write it as a macro. However, since I haven't seen this usage anywhere, I can't publicize it as a design pattern. Anyway, it's my vision to make value polymorphism a design pattern. Who doesn't want to make a little invention
compare
Value polymorphism is between traditional polymorphism and type erasure. Compared with various existing polymorphism implementations in C + +, value polymorphism has a great advantage in its application.
Compared with the traditional polymorphism, the value polymorphism preserves the tool and thinking mode of inheritance, but it is different from the pointer semantics of the traditional polymorphism. The value polymorphism is value semantics, and can be preserved when the value is copied. The meaning of value semantic polymorphism is not only to bring convenience, but also to eliminate potential bug s. Isn't it enough for C/C + + pointers to be criticized?
Compared with type erasure, value polymorphism also uses value semantics (there are reference semantics in the field of type erasure), but instead of duck typing, it chooses a more traditional inheritance. Duck typing is limited everywhere in static type language c + +: instances of type erasure classes can be constructed by duck but cannot be restored; type erasure classes have fixed AF force, such as std::function requiring operator(), that is, using the upper adapter can handle the Shape, but there is no way to deal with the Line and ThickLine of two polymorphic functions. Inheritance, as a native feature of C + +, does not have these problems. More importantly, inheritance is the way of thinking that C + + and many other language programmers are accustomed to.
Compared with polymorphic﹐ value, value polymorphism exchanges universality for runtime performance and implementation freedom -- after all, all classes except SBOData are written by themselves. When type conversion is done, polymorphic_value will match the value of the child, and the polymorphism will not be able to convert it has the final say of the compiler. The types with polymorphic values are completely open to customers, and most of them can be controlled on demand without SBO or SBO, and even can be manually interfered with downward type conversion. The price of freedom, of course, is longer code.
summary
Value polymorphism is a kind of polymorphism implementation way between traditional polymorphism and type erasure. It uses value semantics for reference and retains inheritance. In the scope of application of single inheritance, both programs and programmers can benefit from it. This paper is also the best practice of "Function semantics" chapter in "deep exploration of C + + object model".
For a single-chip computer with a larger memory, there is no bullshit - not enough technology, and the cost comes together.
Reference resources
Polymorphism (computer science) - Wikipedia
Redemption of function/bind (I)