Home > Enterprise >  Difference between template argument deduction for classes and functions
Difference between template argument deduction for classes and functions

Time:10-31

What the problem is:

I'm trying to implement a class that will have two specializations, one for integral types and one for all others. The first version that came to my mind:


#include <type_traits>
template<typename T, typename std::enable_if_t<std::is_integral<T>::value, bool> = true>
class Test
{
};
template<typename T, typename std::enable_if_t<!std::is_integral<T>::value, bool> = true>
class Test
{
};

But GCC fails with the following error when I try to compile the code above:


<source>:2:84: error: template parameter 'typename std::enable_if<std::is_integral<_Tp>::value, bool>::type <anonymous>'
template<typename T, typename std::enable_if_t<std::is_integral<T>::value, bool> = true>
                                                                                   ^~~~
<source>:6:85: note: redeclared here as 'typename std::enable_if<(! std::is_integral<_Tp>::value), bool>::type <anonymous>' 
template<typename T, typename std::enable_if_t<!std::is_integral<T>::value, bool> = true>
                                                                                    ^~~~

However, using a similar technique for functions compiles without problems:


#include <type_traits>
template<typename T, typename std::enable_if_t<std::is_integral<T>::value, bool> = true>
void test()
{
}
template<typename T, typename std::enable_if_t<!std::is_integral<T>::value, bool> = true>
void test()
{
}

What I am trying to achieve:

  1. First of all, I want to understand why the version with functions compiles, but the version with classes does not.
  2. My second goal is to implement a class that satisfies the conditions that I specified at the very beginning.

What I have tried:

Using partial specializations solves problem 2:


#include <type_traits>
template<typename T, bool = std::is_integral<T>::value>
class Test;

template<typename T>
class Test<T, true>
{
};
template<typename T>
class Test<T, false>
{
};

But this approach is bad because it allows to use Test<float, true> and if I understand correctly (please correct me if I'm wrong), then specialization for integral types will be used, which is not what I want.

Summarizing:

I'll just duplicate my goals in the form of questions:

  1. Why does the version with functions compile, but the version with classes doesn't?
  2. How can I implement a class that satisfies the conditions that I specified at the very beginning?

CodePudding user response:

Using std::enable_if in the template parameter list of a class is not guaranteed, since the standard only has SFINAE for functions, not for types. For types it may be available, but only as a non-standard compiler extension.

C 20 adds concepts to the standard, which does work for template parameters on template classes too.

template<class T>
concept NotIntegral = !std::integral<T>;

template<class T>
class Test;

template<std::integral T>
class Test<T>
{
public:
    void operator()() const
    {
        std::cout << "integral\n";
    }
};

template<NotIntegral T>
class Test<T>
{
public:
    void operator()() const
    {
        std::cout << "not integral\n";
    }
};

int main()
{
    std::cout << "float: ";
    (Test<float>{})();
    std::cout << "int: ";
    (Test<int>{})();
}

For prior versions of C you could add a static_assert to the class body which doesn't prevent the user from specifying an invalid value for the second template parameter, but at least compiling the code will yield a compiler error of your choosing:

template<typename T>
class Test<T, true>
{
    static_assert(std::is_integral_v<T>,
        "The second template parameter must contain true if and only if the first template parameter is an integral type");
};

You could introduce a function for creating a the object instead of providing allowing the user to create objects with invalid template parameters though:

template<typename T, bool = std::is_integral<T>::value>
class Test;

template<class T>
Test<T, std::is_integral<T>::value> MakeTest()
{
    return {};
}

template<typename T>
class Test<T, true>
{
    friend Test<T, std::is_integral<T>::value> MakeTest<T>();
    Test() = default;
public:

    void operator()() const
    {
        std::cout << "integral\n";
    }
};
template<typename T>
class Test<T, false>
{
    friend Test<T, std::is_integral<T>::value> MakeTest<T>();
    Test() = default;
public:

    void operator()() const
    {
        std::cout << "not integral\n";
    }
};

int main()
{

    std::cout << "float: ";
    MakeTest<float>()();
    std::cout << "int: ";
    MakeTest<int>()();
    // Test<int, false> test; // would yield a compiler error because of the inaccessible default constructor
}

CodePudding user response:

Without concepts, you can do partial specializations in a more "private" way, like this:

namespace detail {
template<typename T, bool>
class Test;

template<typename T>
class Test<T, true>
{
};

template<typename T>
class Test<T, false>
{
};
} // namespace detail

template<typename T>
using Test = detail::Test<T, std::is_integral<T>::value>;

Side notes: you don't need two partial specializations, you can use the main definition for one of the values true or false (without specifying it), and you might want to rename the main Test class to something else, perhaps TestImpl; it doesn't matter to the user.


I'd like to provide one more alternative. I think it's important to remember that, if your two specializations are fairly similar, you could just use if constexpr (C 17) or tag dispatching instead of partial specializations:

#include <type_traits>
#include <iostream>

template<typename T>
class Test
{
public:
    void operator()() const {
        if constexpr(std::is_integral<T>::value) {
            std::cout << "integral\n";
        } else {
            std::cout << "not integral\n";
        }
    }
};

int main()
{
    Test<float>{}();
    Test<int>{}();
}
  • Related