C++ Core Guidelines: Type Erasure with Templates

Contents[Show]

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.

 bridge

As our starting point and as a 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:

  • Virtual dispatch
  • Intrusive, because the derived class must know about its base

Let's see which drawbacks type erasure with templates solve.

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(const T& obj): object(std::make_shared<Model<T>>(std::move(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. This 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.

typeErasure

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 message, copied from the Compiler Explorer.

I start with the ugliest one from Clang 6.0.0 one and end with the quite good one from  GCC 8.2.  The error message from MSVC 19 is something in between. To be honest, I was totally astonished, because I thought that 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 once screenshot.

errorClang

 

MSVC 19

errorWindows

GCC 8.2

errorGcc

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 and in particular 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 at compile time if a given class has a specific method. 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 which is part of the library fundamental TS v2. To use it, you have to include the header from the experimental namespace (1). 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(const T& obj): object(std::make_shared<Model<T>>(std::move(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 the lines (1), (2), and (3). Line (2) deduces the type of the member function getName(). std::declval from C++11 is a function which allows you to use member functions in decltype expressions without the need 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 the Clang 6.0.0 produces if I execute the program in the Compiler Explorer:

errorClangDetection

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.

errorClangDetectionFocus

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 need not to know the base class

Cons:

  • Virtual dispatch
  • Difficult to implement

In the end, the difference of type erasure with object-orientation and with templates mainly boils down to two points:

  • Intrusive versus non-intrusive
  • Easy versus difficult 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: Eric Pederson, Paul Baxter,  Meeting C++, Matt Braun, Avi Lachmish, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, and Mielo.

 

Get your e-book at Leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 600 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

 Get your interactive course at educative

Modern C++ Concurrency in Practice: Get the most out of any machine

educative

Based on my book "Concurrency with Modern C++" educative.io created an interactive course.

What's Inside?

  • 140 lessons
  • 110 code playgrounds => Run in browser
  • 78 code snippets
  • 55 illustrations

Comments   

+2 #1 Ivan Bobev 2018-09-17 12:18
I think that std::move in the line `Object(const T& obj): object(std::make_shared(std::move(obj))){}` is with no effect, and the right line is: `Object(T&& obj): object(std::make_shared<Model<T>>(std: :forward(obj))){}`.
Quote
0 #2 Ivan Bobev 2018-09-20 00:44
I don't know why I missed the template parameter of std::make_shared from my previous post, both in the quote from the article and from the corrected according to me version, but obviously I wanted to write `Object(T&& obj) : object(std::make_shared(std::forward(obj))){}`. Can you correct it please, because there is no edit section in the site comments? Thanks in advance.
Quote
0 #3 Rainer 2018-09-22 14:08
Quoting Ivan Bobev:
I don't know why I missed the template parameter of std::make_shared from my previous post

That's not your problem. Your text is interpreted. You have to use < or >.
Quote

Add comment


Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 2620

All 1164600

Currently are 130 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments