C++ 模板化代码的语法和语义是什么?

What are the syntax and semantics of C++ templated code?

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(){//...}

我从 c​​pp-reference 复制了这行代码(只有 std::enable_if 部分,我确实需要 T 和所有三个 size_t),因为我会喜欢仅在使用 floating_types 时才使用此功能...它不会编译。

有人可以向我解释一下,为什么,甚至可以做什么吗?当我在做的时候,你之后怎么调用这个函数?

这里关于 SO 的每个教程或问题都会被答案轰炸,这很好,但是对于那些不了解正在发生的事情的人来说,即使这些也不是很有帮助。(对不起,如果可能的话有点激动或有攻击性)

编辑:我非常感谢到目前为止所有的回答,我意识到我的措辞可能有点偏离......我了解模板参数是什么,并且知道运行时和编译时等之间的区别,但我只是无法很好地掌握 std::enable_if

背后的语法

编辑 2:

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();
}

这实际上是我唯一需要更改的地方。注意 random() 部分

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();
}

我会尽可能简单地解释这一点,不要过多地讨论语言细节,因为你要求它。

模板参数是编译时参数(它们在您的应用程序的 运行 期间不会改变)。函数参数是 运行 时间并且有一个内存地址。

调用这个函数看起来像这样:

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

在 <> 括号中,您会看到编译时参数或更准确地说是模板参数,并且本例中的函数在 () 括号中采用 0 运行time 参数。最后一个编译时参数有一个默认参数,用于检查函数是否应该编译(enable_if 类型)。如果您想更清楚地了解什么使您应该搜索术语 SFINAE,这是一种模板元编程技术,用于确定函数或 class 是否应该存在。

这是一个简短的 SFINAE 示例:

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)

第三个函数调用失败的原因是函数不存在。这是因为当作为其模板参数传入的编译时 bool 为 false 时,enable if 导致函数不存在。

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

请注意,很多人都认为 SFINAE 语法很糟糕,并且随着 C++ 20 中概念和约束的引入,将不再需要大量 SFINAE 代码。

首先,我将以工作形式重写您的函数

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
 { }

关键是我已经将 std::enable_if_t 形式 T 的第二个模板参数更改为 int.

我还删除了 std::enable_if_t 之前的 typename 但这并不重要:typename 隐含在 std::enable_if_t 末尾的 _t 中=],从 C++14 引入。在 C++11 中正确的形式是

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

但为什么它有效?

从名字开始:SFINAE。

是"Substitution Failure Is Not An Error"的缩写形式。

这是一个 C++ 规则,所以当你写一些东西时

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

I3std::enable_if_t的条件是true所以std::enable_if_t< I == 3, int>int代替所以foo() 已启用,但当 I 不是 3 时,std::enable_if_t 的条件 if false so std::enable_if_t< I == 3, int> 未被替换,因此 foo() 不是't enabled but this ins' an error (if, through overloading, there is another foo() function, enabled, that matches the call, obviously).

那么你的代码哪里出了问题?

问题在于,当第一个模板参数为 true 时,std::enable_if_t 被替换为第二个参数。

所以如果你写

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

你打电话给

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

the std::is_floating_point<float>::value(但你也可以使用更短的形式 std::is_floating_point_v<T>_v 而不是 ::value))所以替换发生并且你得到

float = 0

但是,不幸的是,模板值(不是类型)参数不能是浮点类型,所以你会得到一个错误。

如果您使用 int 而不是 T,则替换为您

int = 0

这是正确的。

另一种解决方案可以使用以下形式

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

正如 Andreas Loanjoe 所建议的那样,因为替换给了你

typename = float

这是一个有效的语法。

但是这个解决方案有一个缺点,当你想写两个替代函数时,它不起作用,如下例

// 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 ()
 { }

基于值

的解决方案在哪里工作
// 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 ()
 { }

我不会采用自上而下的方法从您的代码片段开始,而是采用自下而上的方法来解释有关模板的一些重要细节以及涉及的工具和技术。


本质上,模板是一种工具,可让您编写适用于 范围 可能类型的 C++ 代码,而不是严格适用于固定类型。在静态类型语言中,这首先是一个在不牺牲类型安全性的情况下重用代码的好工具,但特别是在 C++ 中,模板非常强大,因为它们可以 specialized.

每个模板声明都以关键字 template 开头,然后是 typenon-type 的列表(即 )参数。类型参数使用特殊关键字 typenameclass,用于让您的代码处理一系列类型。非类型参数仅使用现有类型的名称,这些使您可以将代码应用于编译时已知的范围。

一个非常基本的模板化函数可能如下所示:

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
}

这让我们可以针对一系列可能的类型安全地重用代码,我们可以按如下方式使用它:

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

编译器甚至足够聪明,可以为其中的每一个推导出模板参数 T,因此您可以放心地使用以下功能相同的代码:

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

但是假设您希望 print 对一种类型表现不同。例如,假设您有一个需要特殊处理的自定义 Point2D class。您可以使用 模板专业化 :

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 << ')';
}

现在,任何时候我们将 printT=Point2D 一起使用时,都会选择专业化。这非常有用,例如,如果通用模板对一种特定类型没有意义。

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

但是,如果我们想基于一个简单的条件同时为许多 类型特化一个模板怎么办?这就是事情变得有点元的地方。首先,让我们尝试以允许在模板中使用它们的方式来表达条件。这可能有点棘手,因为我们需要编译时答案。

这里的条件是 T 是一个浮点数,如果 T=floatT=double 则为真,否则为假。这实际上很简单,仅通过模板专业化就可以实现。

// 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;
}

现在,我们可以查询任何类型,看它是否是浮点数:

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;

但是我们如何在另一个模板中使用这个编译时条件呢?当有许多可能的模板特化可供选择时,我们如何告诉编译器选择哪个模板?

这是通过利用名为 SFINAE 的 C++ 规则实现的,该规则用基本英语表示,“当有许多可能的模板特化可供选择,而当前的没有意义时*,跳过它并尝试下一个。"

  • 有一个错误列表,在尝试将模板参数替换为模板代码时,会导致模板被忽略而不会立即出现编译器错误。榜单有点long and complex.

模板没有意义的一种可能方式是它尝试使用不存在的类型。

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

这与 std::enable_if 在幕后使用的技巧完全相同。 enable_if 是一个模板 class 接受类型 Tbool 条件,它包含一个嵌套类型 type 等于 T 仅当条件为真时。这也很容易实现:

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
};

现在我们有了一个可以用来代替任何类型的助手。如果我们传递的条件为真,那么我们就可以安全地使用嵌套类型。如果我们传递的条件为假,则不再考虑该模板。

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

我完全同意 std::enable_if<std::is_floating_point<T>::value, void>::type 是拼写类型的一种混乱方式。您可以将其读作“void 如果 T 是浮点数,否则停止并尝试下一个重载”


最后,拆开你的例子:

// 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(){//...}

注意最后的= 0。这只是最后一个模板参数的默认值,它可以让您指定 TMKN 而不是第五个参数。这里使用的 enable_if 意味着您可以提供其他名为 fastor2d 的模板,它们具有自己的条件集。