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

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

本文关键字:语义 是什么 C++ 语法 代码      更新时间:2023-10-16
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(){//...}

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

这实际上是我唯一需要更改的内容。注意随机()部分

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 个运行时参数。最后一个编译时参数有一个默认参数,用于检查函数是否应该编译(enable_if类型)。如果你想更清楚地知道启用,你应该搜索术语SFINAE,这是一种模板元编程技术,用于确定函数或类是否应该存在。

下面是一个简短的 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)

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

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

请注意,很多人都同意SFINAE语法很糟糕,并且在C++ 20中引入概念和约束后,将不再需要许多SFINAE代码。

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


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

每个模板声明都以关键字template开头,以及类型非类型(即)参数的列表。类型参数使用特殊关键字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类。您可以使用模板专用化来执行此操作:

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++规则来实现的,该规则在基本英语中说,"当有许多可能的模板专业化可供选择,而当前的模板没有意义*时,只需跳过它并尝试下一个。

尝试将模板
  • 参数替换为模板化代码时,存在一系列错误,这些错误会导致忽略模板,而不会立即出现编译器错误。该列表有点长且复杂。

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

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

这与std::enable_if在引擎盖下使用的技巧完全相同。enable_if是一个接受类型Tbool条件的模板类,并且它包含一个嵌套类型type当条件为 true 时才等于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
};

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

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是拼写类型的混乱方式。您可以将其读作"voidT 是否为浮点数,否则停止并尝试下一个重载">


最后,拆开你的例子:

// 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的模板,并具有自己的条件集。

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

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

关键是我已经将表单T的第二个模板参数更改为intstd::enable_if_t

我也在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。

是"替换失败不是错误"的缩写形式。

这是一个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时,如果falsestd::enable_if_t< I == 3, int>std::enable_if_t的条件不会被替换,所以foo()没有启用,但这不是一个错误(如果, 通过重载,显然还有另一个启用的foo()函数与调用匹配)。

那么代码中的问题在哪里呢?

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

所以如果你写

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

你打电话

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

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>

正如安德烈亚斯·洛洛乔所建议的那样,因为换人给了你

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