What are the syntax and semantics of C++ templated

2019-05-22 00:24发布

template<typename T, size_t M, size_t K, size_t N, typename std::enable_if_t<std::is_floating_point<T>::value, T> = 0>
void fastor2d(){//...}

I copied this line of code from cpp-reference(only the std::enable_if part, i do need T and all three of the size_t's), because i would like to use this function only when floating_types are used on it ... it does not compile.

Could somebody explain to me, why, and what it even does? While i am at it, how do you call this function afterwards?

Every tutorial or question here on SO gets bombed with answers, and that is great, but to someone who does not understand jacks*** of what is happening, even those are not really helpful.(sry, if possibly slightly agitated or aggressive)

EDIT: i greatly appreciate all answers as of now, i realize that my wording might have been a bit off ... i understand what a template parameter is, and know the difference between runtime and compiletime etc, but i just cant get a good grasp of the syntax behind std::enable_if

EDIT2:

template<typename T, size_t M, size_t K, size_t N, typename = std::enable_if_t<std::is_integral<T>::value>>
void fastor2d(){
    Fastor::Tensor<T,M,K> A; A.randInt();
}

This is literally the only thing i need changed. Notice the random() part

template<typename T, size_t M, size_t K, size_t N, typename = std::enable_if_t<std::is_floating_point<T>::value>>
void fastor2d(){
    Fastor::Tensor<T,M,K> A; A.random();
}

3条回答
在下西门庆
2楼-- · 2019-05-22 00:32

Rather than a top-down approach starting with you code snippet, I'll take a bottom-up approach to explain some important details about templates and what tools and techniques are involved.


At heart, templates are a tool that let you write C++ code that applies to a range of possible types, not strictly for a fixed type. In a statically-typed language, this is firstly a great tool for reusing code without sacrificing type safety, but in C++ in particular, templates are very powerful because they can be specialized.

Every template declaration begins with the keyword template, and a list of type or non-type (i.e value) parameters. Type parameters use the special keyword typename or class, and are used to let your code work over a range of types. Non-type parameters simply use the name of an existing type, and these let you apply your code to a range of values that are known at compile-time.

A very basic templated function might look like the following:

template<typename T> // declare a template accepting a single type T
void print(T t){ // print accepts a T and returns void
    std::cout << t; // we can't know what this means until the point where T is known
}

This lets us reuse code safely for a range of possible types, and we can use it as follows:

int i = 3;
double d = 3.14159;
std::string s = "Hello, world!";
print<int>(i);
print<double>(d);
print<std::string>(s);

The compiler is even smart enough to deduce the template parameter T for each of these, so you can safely get away with the following, functionally identical code:

print(i);
print(d);
print(s);

But suppose you want print to behave differently for one type. Suppose, for example, you have a custom Point2D class that needs special handling. You can do this with a template specialization:

template<> // this begins a (full) template specialization
void print<Point2D>(Point2D p){ // we are specializing the existing template print with T=Point2D
    std::cout << '(' << p.x << ',' << p.y << ')';
}

Now, anytime we use print with T=Point2D, the specialization is chosen. This is really useful, for example, if the generic template just doesn't make sense for one specific type.

std::string s = "hello";
Point2D p {0.5, 2.7};
print(s); // > hello
print(p); // > (0.5,2.7)

But what if we want to specialize a template for many types at once, based on a simple condition? This is where things become a little meta. First, let's try to express a condition in a way that lets them be used inside templates. This can be a little tricky because we need compile-time answers.

The condition here will be that T is a floating point number, which is true if T=float or T=double and false otherwise. This is actually fairly simple to achieve with template specialization alone.

// the default implementation of is_floating_point<T> has a static member that is always false
template<typename T>
struct is_floating_point {
    static constexpr bool value = false;
};

// the specialization is_floating_point<float> has a static member that is always true
template<>
struct is_floating_point<float> {
    static constexpr bool value = true;
};

// the specialization is_floating_point<double> has a static member that is always true
template<>
struct is_floating_point<double> {
    static constexpr bool value = true;
}

Now, we can query any type to see if it's a floating point number:

is_floating_point<std::string>::value == false;
is_floating_point<int>::value == false;
is_floating_point<float>::value == true;
is_floating_point<double>::value == true;

But how can we use this compile-time condition inside another template? How can we tell the compiler which template to choose when there are many possible template specializations to choose from?

This is achieved by taking advantage of a C++ rule called SFINAE, which in basic English, says, "when there are many possible templates, and the current one doesn't make sense*, just skip it and try the next one."

*There's a list of errors, when attempting to substitute template arguments into templated code, that cause the template to be ignored without an immediate compiler error. The list is a bit long and complex.

One possible way that a template doesn't make sense is if it tries to use a type that doesn't exist.

template<T>
void foo(T::nested_type x); // SFINAE error if T does not contain nested_type

This is the exact same trick that std::enable_if uses under the hood. enable_if is a template class accepting a type T and a bool condition, and it contains a nested type type equal to T only when the condition is true. This is also pretty easy to achieve:

template<bool condition, typename T>
struct enable_if {
    // no nested type!
};

template<typename T> // partial specialization for condition=true but any T
struct enable_if<true, T> {
    typedef T type; // only exists when condition=true
};

Now we have a helper that we can use in place of any type. If the condition we pass is true, then we can safely use the nested type. If the condition we pass is false, then the template is no longer considered.

template<typename T>
std::enable_if<std::is_floating_point<T>::value, void>::type // This is the return type!
numberFunction(T t){
    std::cout << "T is a floating point";
}

template<typename T>
std::enable_if<!std::is_floating_point<T>::value, void>::type
numberFunction(T t){
    std::cout << "T is not a floating point";
}

I completely agree that std::enable_if<std::is_floating_point<T>::value, void>::type is a messy way to spell out a type. You can read it as "void if T is floating point, and meaningless nonsense otherwise."


Finally, to take apart your example:

// we are declaring a template
template<
    typename T, // that accepts some type T,
    size_t M,   // a size_t M,
    size_t K,   // a size_t K,
    size_t N,   // a size_t N,
    // and an unnamed non-type that only makes sense when T is a floating point
    typename std::enable_if_t<std::is_floating_point<T>::value, T> = 0
>
void fastor2d(){//...}

Note the = 0 at the end. That's simply a default value for the final template parameter, and it lets you get away with specifying T, M, K, and N but not the fifth parameter. The enable_if used here means that you can provide other templates called fastor2d, with their own sets of conditions.

查看更多
祖国的老花朵
3楼-- · 2019-05-22 00:37

I'll try to explain this as simple as possible not to go into the language details too much since you asked for it.

Template arguments are compile time arguments (they do not change during the run-time of your application). Function arguments are run-time and have a memory address.

Calling this function would look something like this:

fastor2d<Object, 1, 2, 3>();

In the <> brackets you see the compile-time arguments or more accurately the template parameters, and the function in this case takes 0 runtime arguments in the () brackets. The last compile time argument has a default argument which is used to check whether the function should compile at all (enable_if type). If you want to know more clearly what enable if does you should search for the term SFINAE, which is a template metaprogramming technique used to determine whether a function or class should exist or not.

Here is a short SFINAE example:

template<typename T, typename = std::enable_if_t<std::is_floating_point<T>::value>>
void function(T arg)
{
}

function(0.3f);    //OK
function(0.0);     //OK double results in std::is_floating_point<double>::value == true
function("Hello"); //Does not exist (T is not floating point)

The reason the third function call fails, is because the function does not exist. This is because the enable if caused the function not to exist when the compile-time bool that is passed in as its' template argument is false.

std::is_floating_point<std::string>::value == false

Do note that a lot of people agree that the SFINAE syntax is horrible and that a lot of SFINAE code will not be necessary anymore with the introduction of concepts and constraints in C++ 20.

查看更多
做个烂人
4楼-- · 2019-05-22 00:44

First of all, I'll rewrite your function in a working form

template <typename T, size_t M, size_t K, size_t N,
          std::enable_if_t<std::is_floating_point<T>::value, int> = 0>              
void fastor2d() // ..........................................^^^  int, not T
 { }

The point is that I've changed the second template argument of std::enable_if_t form T to int.

I've also removed the typename before std::enable_if_t but isn't important: the typename is implicit in the _t at the end of std::enable_if_t, introduced from C++14. In C++11 the correct form is

// C++11 version
   typename std::enable_if<std::is_floating_point<T>::value, int>::type = 0
// ^^^^^^^^            no _t                                     ^^^^^^

But why it works?

Start from the name: SFINAE.

Is a short form for "Substitution Failure Is Not An Error".

It's a C++ rule so that when you write some thing as

 template <int I, std::enable_if_t< I == 3, int> = 0>
 void foo ()
  { }

and I is 3, the condition of std::enable_if_t is true so std::enable_if_t< I == 3, int> is substituted with int so foo() is enabled but when I isn't 3, the condition of std::enable_if_t if false so std::enable_if_t< I == 3, int> is not substituted so foo() isn't enabled but this ins't an error (if, through overloading, there is another foo() function, enabled, that matches the call, obviously).

So where is the problem in your code?

The problem is that std::enable_if_t is substituted, when the first template parameter is true, with the second parameter.

So if you write

std::enable_if_t<std::is_floating_point<T>::value, T> = 0

and you call

fastor2d<float, 0u, 1u, 2u>();

the std::is_floating_point<float>::value (but you can also use the shorter form std::is_floating_point_v<T> (_v and not ::value)) so the substitution take place and you get

float = 0

but, unfortunately, a template value (not type) parameter can't be of type floating point, so you get an error.

If you use int instead of T, the substitution give you

int = 0

and this is correct.

Another solution can be use the following form

typename = std::enable_if_t<std::is_floating_point<T>::value, T>

as suggested by Andreas Loanjoe, because the substitution give you

typename = float

that is a valid syntax.

But this solution has the drawback that doesn't works when you want to write two alternative functions, as in the following example

// the following solution doesn't works

template <typename T, 
          typename = std::enable_if_t<true == std::is_floating_point<T>::value, int>>
void foo ()
 { }

template <typename T, 
          typename = std::enable_if_t<false == std::is_floating_point<T>::value, int>>
void foo ()
 { }

where works the solution based on the value

// the following works

template <typename T, 
          std::enable_if_t<true == std::is_floating_point<T>::value, int> = 0>
void foo ()
 { }

template <typename T, 
          std::enable_if_t<false == std::is_floating_point<T>::value, int> = 0>
void foo ()
 { }
查看更多
登录 后发表回答