Search code examples
c++template-meta-programmingc++-conceptsc++-templates

How to specialize a method template using concepts?


My goal is simple: I want to specialize a method template for multiple types at once. I've googled around for possible ways to accomplish this task and found that concepts can be used for that.

I've created a simplified test program that resembles my target one to test how concepts work:

#include <iostream>
#include <concepts>

class A {
public:
    std::string getName() const { return "A"; }
};

class B {
public:
    std::string getName() const { return "B"; }
};

class C {
public:
    std::string getName() const { return "C"; }
};

class D {
public:
    std::string getName() const { return "D"; }
};

// Concept for classes C and D
template<typename T>
concept IsCorD = std::same_as<T, C> || std::same_as<T, D>;

// User class with the method template
class User {
public:
    // Primary template declaration
    template<typename T>
    void doSomething();
};


// Specialization for A
template<>
void User::doSomething<A>() {
    std::cout << "Specialized case for A" << std::endl;
}

// Specialization for B
template<>
void User::doSomething<B>() {
    std::cout << "Specialized case for B" << std::endl;
}

// Partial specialization for C and D using concepts
template<IsCorD T>
void User::doSomething<T>() {
    std::cout << "Case for C or D" << std::endl;
}

int main() {
    User user;
    user.doSomething<A>();
    user.doSomething<B>();
    user.doSomething<C>();
    user.doSomething<D>();

    return 0;
}

Here I'm trying to create a method template specialization for both C and D classes using the IsCorD concept. I use the following command to compile it:

g++ main.cpp --std=c++20 -Wall -Wextra -Wpedantic -O3 -o main

And I get the following compilation error:

main.cpp:51:27: error: non-class, non-variable partial specialization ‘doSomething<T>’ is not allowed
   51 | void User::doSomething<T>() {
      |                           ^
main.cpp:51:6: error: no declaration matches ‘void User::doSomething()’
   51 | void User::doSomething<T>() {
      |      ^~~~
main.cpp:33:10: note: candidate is: ‘template<class T> void User::doSomething()’
   33 |     void doSomething();
      |          ^~~~~~~~~~~
main.cpp:29:7: note: ‘class User’ defined here
   29 | class User {
      |       ^~~~

What am I doing wrong? How to get it compiled and working as intended? The expected output is:

Specialized case for A
Specialized case for B
Case for C or D
Case for C or D

Community members have suggested that this question maybe a duplicate of Why can I seemingly define a partial specialization for function templates?. I checked answers to that question, and they suggest to use std::enable_if to work around the forbidden partial specialization. But the answers there don't provide a solution using concepts.


Solution

  • The error occurs because C++ doesn't allow partial specialization of function templates (including member function templates). That means you cannot write a version of doSomething<T>() that is only enabled for types C and D by "partially specializing" the template. Instead of trying to specialize for types C and D partially, you can create separate overloads of your member functions and constrain them using concepts. Here's one thing that you can try to do:

    #include <iostream>
    #include <concepts>
    #include <string>
    
    // Definitions of classes A, B, C, and D.
    class A {
    public:
        std::string getName() const { return "A"; }
    };
    
    class B {
    public:
        std::string getName() const { return "B"; }
    };
    
    class C {
    public:
        std::string getName() const { return "C"; }
    };
    
    class D {
    public:
        std::string getName() const { return "D"; }
    };
    
    // Concept that checks if a type is either C or D.
    template<typename T>
    concept IsCorD = std::same_as<T, C> || std::same_as<T, D>;
    
    class User {
    public:
        // Overload for A
        template<typename T>
        requires std::same_as<T, A>
        void doSomething() {
            std::cout << "Specialized case for A" << std::endl;
        }
    
        // Overload for B
        template<typename T>
        requires std::same_as<T, B>
        void doSomething() {
            std::cout << "Specialized case for B" << std::endl;
        }
    
        // Overload for C and D
        template<typename T>
        requires IsCorD<T>
        void doSomething() {
            std::cout << "Case for C or D" << std::endl;
        }
    };
    
    int main() {
        User user;
        user.doSomething<A>(); // Prints: Specialized case for A
        user.doSomething<B>(); // Prints: Specialized case for B
        user.doSomething<C>(); // Prints: Case for C or D
        user.doSomething<D>(); // Prints: Case for C or D
        return 0;
    }
    

    Instead, define multiple overloaded versions of doSomething that are constrained using the requires clause and concepts:

    One overload for type A (using requires std::same_as<T, A>).
    One overload for type B.
    One overload for types C or D (using the IsCorD concept).
    

    So the output should look like this:

    Specialized case for A
    Specialized case for B
    Case for C or D
    Case for C or D
    

    Hope this helps!