C++ Core Guidelines: Type Erasure with Templates
In the last post C++ Core Guidelines: Type Erasure, I presented two ways to implement type erasure: void pointers and object orientation. In this post, I bridge dynamic polymorphism (object orientation) with static polymorphism (templates) to get type erasure with templates.
As our starting point and reminder, here is type erasure based on object orientation.
Type erasure with object-orientation
Type erasure with object-orientation boils down to an inheritance hierarchy.
// typeErasureOO.cpp #include <iostream> #include <string> #include <vector> struct BaseClass{ // (2) virtual std::string getName() const = 0; }; struct Bar: BaseClass{ std::string getName() const override { return "Bar"; } }; struct Foo: BaseClass{ std::string getName() const override{ return "Foo"; } }; void printName(std::vector<const BaseClass*> vec){ // (3) for (auto v: vec) std::cout << v->getName() << std::endl; } int main(){ std::cout << std::endl; Foo foo; Bar bar; std::vector<const BaseClass*> vec{&foo, &bar}; // (1) printName(vec); std::cout << std::endl; }
The key point is that you can use instances of Foo
or Bar
instead of an instance for BaseClass.
For further details, read the post C++ Core Guidelines: Type Erasure.
What are the pros and cons of this implementation with OO?
Pros:
- Typesafe
- Easy to implement
Cons:
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
- Virtual dispatch
- Intrusive because the derived class must know about its base
Let’s see which drawbacks type erasure with templates solves.
Type erasure with templates
Here is the templates program, which corresponds to the previous OO program.
// typeErasure.cpp #include <iostream> #include <memory> #include <string> #include <vector> class Object { // (2) public: template <typename T> // (3) Object(T&& obj): object(std::make_shared<Model<T>>(std::forward<T>(obj))){} std::string getName() const { // (4) return object->getName(); } struct Concept { // (5) virtual ~Concept() {} virtual std::string getName() const = 0; }; template< typename T > // (6) struct Model : Concept { Model(const T& t) : object(t) {} std::string getName() const override { return object.getName(); } private: T object; }; std::shared_ptr<const Concept> object; }; void printName(std::vector<Object> vec){ // (7) for (auto v: vec) std::cout << v.getName() << std::endl; } struct Bar{ std::string getName() const { // (8) return "Bar"; } }; struct Foo{ std::string getName() const { // (8) return "Foo"; } }; int main(){ std::cout << std::endl; std::vector<Object> vec{Object(Foo()), Object(Bar())}; // (1) printName(vec); std::cout << std::endl; }
Okay, what is happening here? Don’t be irritated by the names Object
, Concept
, and Model
. They are typically used for type erasure in the literature. So I stick to them.
First of all. My std:
:vector uses instances (1) of type Object
(2) and not pointers, such as in the first OO example. These instances can be created with arbitrary types because it has a generic constructor (3). Object has the getName
method (4), which is directly forwarded to the getName
of object. object is of type std::shared_ptr<const Concept>
. The getName
method of Concept
is pure virtual (5). Therefore, due to virtual dispatch, the getName
method of Model
(6) is used. In the end, the getName
methods of Bar
and Foo
(8) are applied in the printName
function (7).
Here is the output of the program.
Of course, this implementation is type-safe.
Error messages
I’m currently giving a C++ class. We quite often have discussions about error messages with templates; therefore, I was curious about the error messages if I change the classes Foo
and Bar
a little bit. Here is the incorrect implementation:
struct Bar{ std::string get() const { // (1) return "Bar"; } }; struct Foo{ std::string get_name() const { // (2) return "Foo"; } };
I renamed the method getName
to get
(1) and to get_name (2).
Here are the error messages copied from the Compiler Explorer.
I start with the ugliest one from Clang 6.0.0 and end with the quite good one from GCC 8.2. The error message from MSVC 19 is something in between. I was astonished because I thought clang would produce the clearest error message.
Clang 6.0.0
I can only display half of the error message because it’s too much for one screenshot.
MSVC 19
GCC 8.2
Please look carefully at the screenshot of GCC 8.2. It says: “27:20: error: ‘const struct Foo’ has no member named ‘getName’; did you mean ‘get_name’?”. Isn’t that great?
The error message from MSVC, particularly from Clang, is quite bad. This should not be the end of my post.
My Challenge
Now I want to solve the challenge: How can I detect if a given class has a specific method at compile time? In our case, the classes Bar
and Foo
should have a method getName.
I played with SFINAE, experimented with the C++11 variant std::enable_if, and ended with the detection idiom, part of the library fundamental TS v2. You must include the header from the experimental namespace (1) to use it. Here is the modified example:
// typeErasureDetection.cpp #include <experimental/type_traits> // (1) #include <iostream> #include <memory> #include <string> #include <vector> template<typename T> using getName_t = decltype( std::declval<T&>().getName() ); // (2) class Object { public: template <typename T> Object(T&& obj): object(std::make_shared<Model<T>>(std::forward<T>(obj))){ // (3) static_assert(std::experimental::is_detected<getName_t, decltype(obj)>::value, "No method getName available!"); } std::string getName() const { return object->getName(); } struct Concept { virtual ~Concept() {} virtual std::string getName() const = 0; }; template< typename T > struct Model : Concept { Model(const T& t) : object(t) {} std::string getName() const override { return object.getName(); } private: T object; }; std::shared_ptr<const Concept> object; }; void printName(std::vector<Object> vec){ for (auto v: vec) std::cout << v.getName() << std::endl; } struct Bar{ std::string get() const { return "Bar"; } }; struct Foo{ std::string get_name() const { return "Foo"; } }; int main(){ std::cout << std::endl; std::vector<Object> vec{Object(Foo()), Object(Bar())}; printName(vec); std::cout << std::endl; }
I added lines (1), (2), and (3). Line (2) deduces the member function type. std::declval from C++11 is a function that allows you to use member functions in expressions without needing to construct the object. The crucial part of the detection idiom is the function std::experimental::is_detected
from the type traits library in the static_assert
(3).
Let’s see what Clang 6.0.0 produces if I execute the program in the Compiler Explorer:
Wow! That is still too much output. To be honest. The state of the feature is still experimental. If you look carefully at the output of the error message and you search for static_assert,
, you find the answer you are looking for. Here are the first three lines of the output.
Great! At least you can grep for the string.”No method getName available
” in the error message.
Before I end the post, here are the pros and cons of type erasure with templates:
Pros:
- Typesafe
- Non-intrusive because the derived class doesn’t need to know the base class
Cons:
- Virtual dispatch
- Difficult to implement
In the end, the difference between type erasure with object orientation and with templates mainly boils down to two points:
- Intrusive versus non-intrusive
- Easy versus challenging to implement
What’s next?
This is the end of my detour. in the next post, I will continue my journey through generic programming; to be more specific, I will write about concepts.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!