Template specialization VS CRTP VS Pure virtual functions

Hello everyone,

I am currently working on a resource manager class to handle different kinds of assets: textures, font, sounds...

In order to handle the different types of resource, I'm using a class template. Most of the methods are common no matter the resource type but I need specific logic to load them.

Based on what I have read here and there, I can think of 3 ways to do this:
* Template specialization
* CRTP
* Derived class and pure virtual function

I tried to illustrate those 3 options with the 3 following examples where f1 represent a function common to all the types and f2 a function that would need to be defined based on each type.

Template specialization:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>

template<typename T>
class BaseClass {
public:
    BaseClass(T val)
        : myVar(val)
    {}

    void f1() // Common implementation no matter type T
    {
        std::cout << "Call to f1" << std::endl;
    }

    void f2(); // No general implementation => one implementation per type T

private:
    T myVar;
};

template<>
void BaseClass<int>::f2()
{
    std::cout << "Call to f2 for int" << std::endl;
}

template<>
void BaseClass<float>::f2()
{
    std::cout << "Call to f2 for float" << std::endl;
}



int main()
{
    BaseClass<int> myIntObj(5);
    BaseClass<float> myFloatObj(5.f);
    BaseClass<double> myDoubleObj(5); // Possible to create the object even if the function f2 is not defined for that type

    myIntObj.f2();
    myFloatObj.f2();
    //myDoubleObj.f2(); // Build error => f2 does not exist for type double

    return 0;
}


CRTP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <iostream>


template<typename Derived, typename T>
class BaseClass {
public:
    BaseClass(T val)
        : myVar(val)
    {}

    void f1() // Common implementation no matter type T
    {
        std::cout << "Call to f1" << std::endl;
    }

    void f2(const std::string& l_path) { // No general implementation => one implementation per type T
        return static_cast<Derived*>(this)->f2(l_path);
    }

private:
    T myVar;
};


class MyIntClass : public BaseClass<MyIntClass, int>
{
public:
    MyIntClass(int val)
        : BaseClass(val)
    {}

    void f2()
    {
        std::cout << "Call to f2 for int" << std::endl;
    }
};


class MyFloatClass : public BaseClass<MyFloatClass, float>
{
public:
    MyFloatClass(float val)
        : BaseClass(val)
    {}

    void f2()
    {
        std::cout << "Call to f2 for float" << std::endl;
    }
};


class MyDoubleClass : public BaseClass<MyDoubleClass, double>
{
public:
    MyDoubleClass(double val)
        : BaseClass(val)
    {}

    // No def for f2
};



int main()
{
    MyIntClass myIntObj(5);
    MyFloatClass myFloatObj(5.f);
    MyDoubleClass myDoubleObj(5); // No build error even though f2 function is not defined for that type

    myIntObj.f2();
    myFloatObj.f2();
    //myDoubleObj.f2(); // Build error => f2 not defined for class MyDoubleClass

    return 0;
}


Derived class and pure virtual function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>


template<typename T>
class BaseClass {
public:
    BaseClass(T val)
        : myVar(val)
    {}

    void f1() // Common implementation no matter type T
    {
        std::cout << "Call to f1" << std::endl;
    }

    virtual void f2() = 0; // No general implementation => one implementation per type T

private:
    T myVar;
};


class MyIntClass : public BaseClass<int>
{
public:
    MyIntClass(int val)
        : BaseClass(val)
    {}

    void f2()
    {
        std::cout << "Call to f2 for int" << std::endl;
    }
};

class MyFloatClass : public BaseClass<float>
{
public:
    MyFloatClass(float val)
        : BaseClass(val)
    {}

    void f2()
    {
        std::cout << "Call to f2 for float" << std::endl;
    }
};

class MyDoubleClass : public BaseClass<double>
{
public:
    MyDoubleClass(double val)
        : BaseClass(val)
    {}

    //No definition of f2
};





int main()
{
    MyIntClass myIntObj(5);
    MyFloatClass myFloatObj(5.f);
    //MyDoubleClass myDoubleObj(5); // Build error => pure virtual function not implemented in the class MyDoubleClass

    myIntObj.f2();
    myFloatObj.f2();
    //myDoubleObj.f2(); // Build error => pure virtual function not implemented in the class MyDoubleClass

    return 0;
}


All those examples are giving the same output.

The question is a bit general but I read a book in which they were using the CRTP technique and can't figure out why they would not simply use template specialization. I could not find anything that I think would not work with it.

What would be the pros and cons of those 3 techniques?

From what I read one advantage of CRTP compare to virtual functions is that it avoid runtime polymorphism and thus improve performance.

Also I was a bit surprised to see that the pure virtual function example was compiling since I have read several times that template are not compatible with pure virtual function. Is it something that newer compiler can handle and that older one could not?

Thank you in advance,

Have a great day!
Last edited on
Template specialization: Is used when different types needs different handling. The downside is of course that you need to provide these specializations. So it has limited use.


CRTP: Is an optimization technique. You may use it when calling the function(s) is indeed a bottleneck. The limit here is that you can only 'simulate' one level of abstraction. More is possible but it gets awkward very quickly.


Derived class and pure virtual function: This is used if you want to extend the functionality of a class and there might be more than one level of inheritance. It is usually not used with template.
In order to handle the different types of resource, I'm using a class template. Most of the methods are common no matter the resource type but I need specific logic to load them.

Okay, but what is the role of the class template?

To me, an obvious solution involves a set of free functions named something like load_png, load_obj, load_anim.
Why isn't this good enough? Why did you choose to metaprogram a solution?

The question is a bit general but I read a book in which they were using the CRTP technique and can't figure out why they would not simply use template specialization.

CRTP is characterized by a set of class hierarchies where each base class depends upon its derived classes. If you don't make use of this property there is no point in using CRTP.

Also I was a bit surprised to see that the pure virtual function example was compiling since I have read several times that template are not compatible with pure virtual function.
Your sources are probably telling you that member function templates cannot be virtual. The following code falls afoul of this rule:
class A { template <typename> virtual void f() {}; };
Last edited on
Thank you both for your inputs.

@coder777, I think I understand all you wrote but it is still not clear for me what would be the advantages to use one solution over the others. Maybe I am missing a subtle element there?

To me, an obvious solution involves a set of free functions named something like load_png, load_obj, load_anim.
Why isn't this good enough? Why did you choose to metaprogram a solution?

The thing is I need to be able to deal with different types of resources (and so datatype) but most of the logic is identical, only the loading mechanism depends on the type of resource I'm using.

Given this, the first thing I thought of was to specialized the load() member method. This way, I simply need to write all the other member methods just once (no matter the resource type) and write specific logic for all the different resource types for the load() method.

To give you a more concrete example, I may have something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
template<typename T>
class ResourceManager{
public:
    ResourceManager() {...}
    ~ResourceManager() {...}

    T* getResource(const std::string& id) {...} //Logic is the same no matter the resource type
    void purgeResources() {...}                 //Logic is the same no matter the resource type

    T* load(const std::string& filePath);       //Logic depends on the resource type => in this case, the method is specialized

private:
    std::unordered_map<std::string, T*> resources;
};

template<>
Texture* ResourceManager<Texture>::load(const std::string& filePath)
{
    //Specific loading logic for textures
}

template<>
Sound* ResourceManager<Sound>::load(const std::string& filePath)
{
    //Specific loading logic for sounds
}


class myClass{
public:
    myClass();
    ~myClass();
    ...
    
private:
    ResourceManager<Texture> textureManager;
    ResourceManager<Sound> soundManager;
};


I don't really follow how I could get away with this writing a set of free functions as you call them. Is there something I am missing here?


To build up on that example, this is how it would look using CRTP method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
template<typename Derived, typename T>
class ResourceManager{
public:
    ResourceManager() {...}
    ~ResourceManager() {...}

    T* getResource(const std::string& id) {...} //Logic is the same no matter the resource type
    void purgeResources() {...}                 //Logic is the same no matter the resource type

    T* load(const std::string& filePath){       //Logic depends on the resource type => in this case, CRTP method is used
	return static_cast<Derived*>(this)->Load(l_path);
    }

private:
    std::unordered_map<std::string, T*> resources;
};


class TextureManager: public ResourceManager<TextureManager, Texture>
{
public:
  TextureManager(): ResourceManager(){}

  sf::Texture* load(const std::string& filePath){
    //Specific loading logic for textures
  }
};


class SoundManager: public ResourceManager<SoundManager, Sound>
{
public:
  SoundManager(): ResourceManager(){}

  sf::Texture* load(const std::string& filePath){
    //Specific loading logic for sounds
  }
};


class myClass{
public:
    myClass();
    ~myClass();
    ...
    
private:
    TextureManager textureManager;
    SoundManager soundManager;
};


To be a bit more clear in my question, is there any reasons why choosing the CRTP option rather than the specialization one?
To be a bit more clear in my question, is there any reasons why choosing the CRTP option rather than the specialization one?

No, because CRTP is more complicated than simple specialization.

However, you've missed the point of CRTP. Plain inheritance is sufficient to solve your problem.

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
  class resource_manager_base
  { 
  protected:
    std::unordered_map<std::string, T> resources;
    ~ resource_manager_base() = default; 
  public:
    T const& get(std::string_view name) const { /* ... */ }
  };

struct texture_manager : resource_manager_base<sf::Texture>
{ void load(std::filesystem::path p) { /* ... */ } };


I don't really follow how I could get away with this writing a set of free functions as you call them. Is there something I am missing here?

The less code in your program the fewer opportunities are available to make errors. Don't create nonessential problems just to solve them and make mistakes in the process.

resource_manager apparently imposes no invariants on its member data. Therefore the member data should be public.
1
2
3
4
5
6
7
template<typename T>
  struct resource_manager
  {
    std::unordered_map<std::string, T> resources;
    T const& get(std::string_view name) const { /* ... */ }
    T load(std::string_view path) = delete; // specializations follow
  };

The member functions don't require access to the class's private representation any more, so they should be non-member non-friends.
1
2
3
4
5
6
7
8
9
10
template<typename T>
  struct resource_manager
  {
    std::unordered_map<std::string, T> resources;
  };

template <typename T>
  T const& get_resource(resource_manager<T> const& rs, std::string_view name);
template <typename T>
  T load_resource(std::string_view path) = delete;

Now the class definition is redundant and should be removed.
1
2
3
4
template <typename T>
  T const& get_resource(std::unordered_map<std::string, T> const& rs, std::string_view name);
template <typename T> // primary template; specializations follow
  T load_resource(std::string_view path) = delete;

Now remove do-nothing function templates (if any) whose functionality is provided by the standard library. Maybe get_resource<T> is simply a call to find and should therefore be removed.
1
2
template <typename T> // primary template; specializations follow
  T load_resource(std::string_view path) = delete

Yield to the longstanding advice against specializing function templates and rename them instead
1
2
sf::Texture load_texture(std::string_view path);
sf::Other load_other(std::string_view path);

Is this part of SFML? If so, throw it away and call sf::load_whatever.
It's all gone. If anything is left, consider generalizing so the user's not locked into std::unordered_map.

Last edited on
but it is still not clear for me what would be the advantages to use one solution over the others
Well, there is nothing like the one solution that solves all of your problems. Sometimes you need a mix of them.

So what approach to use depends on your requirements.

In your case the class ResourceManager seems to extend the functionality of unordered_map. You might not need a virtual function at all. Maybe like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<typename Derived, typename T>
class ResourceManager{
public:
    ResourceManager() {...}
    ~ResourceManager() {...}

    T* getResource(const std::string& id) {...} //Logic is the same no matter the resource type
    void purgeResources() {...}                 //Logic is the same no matter the resource type

    T* load(const std::string& filePath){       //Logic depends on the resource type => in this case, CRTP method is used
	return static_cast<Derived*>(this)->Load(l_path);
    }

private:
    std::unordered_map<std::string, T*> resources;
};


struct TextureManager: ResourceManager<Texture>
{
  sf::Texture* load(const std::string& filePath){
    //Specific loading logic for textures
  }
};
The specific function lead is only called in the specific context and hence having a base function is unnecessary.
Thank you both again for your answers.

I can see how the book I'm reading is overcomplicating some stuff... Maybe there is a reason, but I couldn't find any thus the origin of that thread.

For now I'll go my own more simplistic way thanks to your examples. Let's see where it brings me and maybe I will realize why they used CRTP later in the book for some specific use cases I am not thinking of right now.
Topic archived. No new replies allowed.