Reputation: 2992
Lets assume, that I have a template function foo(T)
and I only want to accept integer types for T
. So the main.cpp
might look something like this:
int main()
{
int i = 1;
foo(i); // Should work fine
foo(&i); // Should not compile
}
Now, there are two options to achieve this, that I am aware of:
static_assert
#include <type_traits>
template<typename T> void foo(T value) {
static_assert(std::is_integral<T>::value, "Not integral!");
// Logic goes here
}
This option has the advantage, that I am able to specify an error message.
The disadvantage is that my g++ 4.9.3 does produce a lot of error output for discrepancies among my type T
and my // Logic
.
std::enable_if
in template argument list#include<type_traits>
template<typename T, class = typename std::enable_if<std::is_integral<T>::value>::type>
void foo(T value) {
// Logic goes here
}
The disadvantage is that the error output produced is not very descriptive of the problem, the advantage is that there is a lot less of it. (The amount of error output could be reduced by combining both approaches.)
For completeness, are there any other (good) ways of achieving the same result?
What option should take less compile time?
As far as I know, both approaches are equivalent in what types they allow in foo()
. Are they?
What I am asking is an expert opinion on "how should this be done in a good code" and what are the conditions of when to use what approach.
Thank you for any input.
Upvotes: 2
Views: 1413
Reputation: 3950
Another option is to let the compiler only specialise for the types you need.
Explaination using example: Normally you need to make the implementation available to the compiler for all template classes and functions:
template<typename T>
class MyClass {
public:
MyClass(T arg) { d_val = arg; }
privateL
T d_val;
};
So the following will work since the compiler will be able to implement all of them:
MyClass<double> classDouble;
MyCalss<int> classInt;
But if you do not provide the implementations then the compiler can't implement them, so you do:
MyClass.h
template<typename T>
class MyClass {
public:
MyClass(T arg);
privateL
T d_val;
};
MyClass.cpp
template<typename T>
MyClass::MyClass() { d_val = arg; }
/* The explicitly make the one for int */
MyClass<int> tmpInt;
And to use:
MyCalss<int> classInt; // Will compile since the compiler already generated the code when it compiled the cpp file
MyClass<double> classDouble; // Wont compile since the compiler does not know how to produce the code.
The issue with this approach is that you need to specify each type you want in the source file (cpp).
I use this approach for a templated class for 16 different types. It is a bit of an issue to add new types but it saves me a lot of code to not redo things. It is a very specific problem with a very specific solution.
Upvotes: 1
Reputation: 5127
In order to illustrate different techniques of achieving your goal, I have changed the example significantly. I hope it still preserves the spirit of your question.
Suppose I have a type for storing rational numbers and I want it to be implicitly convertible from int
:
struct Rational1
{
int i_;
Rational1(int i) : i_(i) {}
};
Now, it works, but now (because of the implicit conversions) it is also convertible from double
and we do not want it:
Rational1 r = 2.5; // BUG (will be interpreted as 2.0)
I can think of three different ways of preventing it (you have already mentioned 2):
struct Rational2
{
int i_;
template <typename T>
Rational2(T i) : i_(i)
{ static_assert(std::is_same<T, int>::value, "msg"); }
};
struct Rational3
{
int i_;
template <typename T, typename std::enable_if<std::is_same<T, int>::value, int>::type = 0>
Rational3(T i) : i_(i) {}
};
struct Rational4
{
int i_;
Rational4(int i) : i_(i) {}
template <typename T>
Rational4(T i) = delete;
};
Tests it:
Rational2 r = 2.5; // compile-time error
Rational3 r = 2.5; // compile-time error
Rational4 r = 2.5; // compile-time error
But, if you check with std::is_convertible
the result is different:
static_assert(std::is_convertible<double, Rational2>::value = true, "");
static_assert(std::is_convertible<double, Rational3>::value = false, "");
static_assert(std::is_convertible<double, Rational4>::value = false, "");
This shows that Rational2
is convertible form double
: the constructor template is selected and compiled. The standard requires that the static_assert
inside triggers a compile time error (and with the message of your choosing), but it does not stop the compilation: this is why you see further messages.
Case Rational3
with enable_if
makes the template invisible for types other than int
so, it cannot further check for errors inside the function, but the compiler can look for other constructors, and perhaps select another one.
Case Rational4
explicitly states that if we try to convert from anything else but int
it should be treated as hard error: and this is also visible to std::is_convertible
.
(However you cannot use this technique in your example, where you constrain to a meta-function (is_integral
) rather than to a concrete type.)
Upvotes: 1