普通模板在哪里结束,元模板从哪里开始
Where do normal templates end and meta templates begin?
Jörg对这个问题的回答很好地描述了在数据上运行的"普通"模板(问题所指的泛型,也许是错误的泛型)和在程序上运行的元模板。Jörg然后明智地提到程序是数据,所以它实际上都是一回事。也就是说,元模板仍然是一个不同的野兽。普通模板在哪里结束,元模板从哪里开始?
我能想出的最好的测试是模板的参数是否完全class
或typename
模板是"正常的",否则是元的。这个测试正确吗?
边界:具有逻辑行为的签名
好吧,在我看来,边界线是绘制的,模板的签名停止为产生运行时代码的简单签名,并成为显式或隐式逻辑的定义,将在编译时执行/解析。
一些例子和解释
常规模板,即仅具有类型名、类或可能的值类型模板参数,一旦在编译时实例化,就会生成可执行的 cpp 代码。
代码(重要)在编译时未执行
例如(非常简单且很可能是不切实际的示例,但解释了概念):
template<typename T>
T add(const T& lhs, const T& rhs) {
return(lhs + rhs);
}
template<>
std::string add<std::string>(
const std::string& lhs,
const std::string& rhs) {
return (lhs.append(rhs));
}
int main() {
double result = add(1.0, 2.0); // 3.0
std::string s = add("This is ", " the template specialization...");
}
编译完成后,根模板将用于实例化 double 类型的上述代码,但不会执行它。此外,专用化模板将实例化用于文本串联,但也: 不会在编译时执行。
但是,此示例:
#include <iostream>
#include <string>
#include <type_traits>
class INPCWithVoice {
void doSpeak() { ; }
};
class DefaultNPCWithVoice
: public INPCWithVoice {
public:
inline std::string doSpeak() {
return "I'm so default, it hurts... But at least I can speak...";
}
};
class SpecialSnowflake
: public INPCWithVoice {
public:
inline std::string doSpeak() {
return "WEEEEEEEEEEEH~";
}
};
class DefaultNPCWithoutVoice {
public:
inline std::string doSpeak() {
return "[...]";
}
};
template <typename TNPC>
static inline void speak(
typename std::enable_if<std::is_base_of<INPCWithVoice, TNPC>::value, TNPC>::type& npc)
{
std::cout << npc.doSpeak() << std::endl;
};
int main()
{
DefaultNPCWithVoice npc0 = DefaultNPCWithVoice();
SpecialSnowflake npc1 = SpecialSnowflake();
DefaultNPCWithoutVoice npc2 = DefaultNPCWithoutVoice();
speak<DefaultNPCWithVoice>(npc0);
speak<SpecialSnowflake>(npc1);
// speak<DefaultNPCWithoutVoice>(npc2); // Won't compile, since DefaultNPCWithoutVoice does not derive from INPCWithVoice
}
此示例显示了模板元编程(实际上是一个简单的示例... 这里发生的情况是,"speak"函数有一个模板化参数,如果在编译时解析并衰减到 TNPC,如果为其传递的类型派生自 INPCWithVoice。
这反过来意味着,如果没有,模板将没有实例化的候选对象,并且编译已经失败。 查找SFINAE以获取此技术: http://eli.thegreenplace.net/2014/sfinae-and-enable_if/
此时,在编译时执行了一些逻辑,整个程序一旦链接到可执行文件/库,就会完全解决
另一个很好的例子是:https://akrzemi1.wordpress.com/2012/03/19/meta-functions-in-c11/
在这里,您可以看到阶乘函数的模板元编程实现,证明如果元模板衰减为常数,即使是字节码也可以完全等于固定值的使用。
最终确定示例:斐波那契
#include <iostream>
#include <string>
#include <type_traits>
template <intmax_t N>
static unsigned int fibonacci() {
return fibonacci<N - 1>() + fibonacci<N - 2>();
}
template <>
unsigned int fibonacci<1>() {
return 1;
}
template <>
unsigned int fibonacci<2>() {
return fibonacci<1>();
}
template <intmax_t MAX>
static void Loop() {
std::cout << "Fibonacci at " << MAX << ": " << fibonacci<MAX>() << std::endl;
Loop<MAX - 1>();
}
template <>
void Loop<0>() {
std::cout << "End" << std::endl;
}
int main()
{
Loop<10>();
}
此代码仅为位置 N 处的斐波那契数列实现标量模板参数模板元编程。 此外,它还显示了从 10 到 0 的循环计数的编译时!
最后
我希望这能澄清一些事情。
但请记住:循环和斐波那契示例为每个索引实例化上述模板!!
因此,存在可怕的冗余和二进制膨胀!!
我自己不是专家,我确信 stackoverflow 上有一个模板元编程功夫大师,他可以附加任何缺少的必要信息。
尝试区分和定义术语
让我们首先尝试粗略地定义这些术语。我从"编程"的足够好的定义开始,然后反复应用meta-
的"通常"含义:
编程
编程会产生一个转换某些数据的程序。
int add(int value) { return value + 42; }
我刚刚编写了代码,该代码将导致一个程序将一些数据(整数)转换为其他一些数据。
模板(元编程)
元编程产生一个"程序",将某个程序转换为另一个程序。使用C++模板,没有有形的"程序",它是编译器工作的隐含部分。
template<typename T>
std::pair<T,T> two_of_them(T thing) {
return std::make_pair(thing, thing);
}
我只是编写了代码来指示编译器的行为类似于发出(代码)另一个程序的程序。
元模板(元元编程?
编写元模板会产生一个"程序",该"程序"会产生一个"程序",从而产生一个程序。因此,在C++中,编写生成新模板的代码。(来自我的另一个答案:)
// map :: ([T] -> T) -> (T -> T) -> ([T] -> T)
// "List" "Mapping" result "type" (also a "List")
// --------------------------------------------------------
template<template<typename...> class List,
template<typename> class Mapping>
struct map {
template<typename... Elements>
using type = List<typename Mapping<Elements>::type...>;
};
这是编译器如何将两个给定模板转换为新模板的描述。
可能的反对意见
看看其他答案,有人可能会争辩说,我的元编程示例不是"真正的"元编程,而是"泛型编程",因为它没有在"元"级别实现任何逻辑。但是,给出的编程示例可以被认为是"真正的"编程吗?它也没有实现任何逻辑,它是从数据到数据的简单映射,就像元编程示例实现从代码(auto p = two_of_them(42);
)到代码(模板"填充"了正确的类型)的简单映射一样。
因此,IMO,添加条件(例如通过专业化)只会使模板更加复杂,但不会改变其性质。
您的测试
绝对没有。考虑:
template<typename X>
struct foo {
template<typename Y>
using type = X;
};
foo
是具有单个typename
参数的模板,但模板中的"结果"(名为foo::type
...只是为了保持一致性),即"结果" - 无论给出什么参数 - 给foo
的类型(从而给行为,该类型实现的程序)。
让我开始使用 dictionary.com 中的定义来回答
定义
元-
一个前缀添加到一个主题的名称中,并指定另一个分析原始主题但在更抽象、更高层次上的主题:元哲学;金属语言学。
添加到某物名称中的前缀,有意识地引用或评论其自己的主题或特征:元画 艺术家画画布。
模板编程最常用作在C++类型系统中表达关系的一种方式。因此,我认为可以公平地说,模板编程本质上利用了类型系统本身。
从这个角度来看,我们可以相当直接地应用上面给出的定义。模板编程和元(模板)编程之间的区别在于模板参数的处理和预期结果。
检查其参数的模板代码显然属于前者定义,而从模板参数创建新类型可以说属于后者。请注意,这还必须与代码对类型进行操作的意图相结合。
例子
让我们看一些例子:
实施标准::aligned_storage;
template<std::size_t Len, std::size_t Align /* default alignment not implemented */>
struct aligned_storage {
typedef struct {
alignas(Align) unsigned char data[Len];
} type;
};
此代码满足第二个条件,即类型std::aligned_storage
用于创建另一个类型。我们可以通过创建一个包装器来使其更加清晰
template<typename T>
using storage_of = std::aligned_storage<sizeof(T), alignof(T)>::type;
现在我们满足上述两个条件,我们检查参数类型 T,提取它的大小和连接,然后我们使用该信息来构造一个依赖于我们的参数的新类型。这显然构成了元编程。
最初的std::aligned_storage
不太清楚,但仍然相当普遍。我们以类型的形式提供结果,并且这两个参数都用于创建新类型。可以说,检查是在创建内部数组类型的type::data
时发生的。
论证完整性的反例:
template<
class T,
class Container = std::vector<T>,
class Compare = std::less<typename Container::value_type>
> class priority_queue { /*Implementation defined implementation*/ };
在这里,您可能会有以下问题:
但是优先级队列不是也执行类型检查,例如检索底层容器或评估其迭代器的类型吗?
是的,确实如此,但目标是不同的。类型std::priority_queue
本身并不构成元模板编程,因为它不利用信息在类型系统中进行操作。同时,以下内容将是元模板编程:
template<typename C>
using PriorityQueue = std::priority_queue<C>;
此处的目的是提供类型,而不是对数据本身的操作。当我们查看可以对每个代码进行的更改时,这一点会变得更加清晰。
我们可以更改std::priority_queue
的实现,也许可以更改允许的操作。例如,支持更快的访问、附加操作或容器内位的紧凑存储。但所有这些都完全是为了实际的运行时功能,而不关心类型系统。
相比之下,看看我们可以对PriotityQueue做些什么。如果我们要选择不同的底层实现,例如,如果我们发现我们更喜欢Boost.Heap,或者我们无论如何都链接到Qt并希望选择它们的实现,那就是单行更改。这就是元编程的目的,我们在由其他类型形成的基于类型系统的参数中做出选择。
(元)模板签名
关于你的测试,正如我们上面看到的,storage_of
只有typename参数,但很明显是元编程。如果你挖掘deaper,你会发现类型系统本身,带有模板,图灵完备。甚至不需要明确陈述任何积分变量,例如,我们可以轻松地用递归堆叠模板替换它们(即自然数的 Zermelo 构造)
using Z = void;
template<typename> struct Zermelo;
template<typename N> using Successor = Zermelo<N>;
在我看来,更好的测试是询问给定的实现是否具有运行时效果。如果模板结构或别名不包含任何仅在运行时发生效果的定义,则可能是模板元编程。
结语
当然,正常的模板编程可能会使用元模板编程。可以使用元模板编程来确定普通模板参数的属性。
例如,您可以选择不同的输出策略(假设某些元编程实现template<class Iterator> struct is_pointer_like;
template<class It> generateSomeData(It outputIterator) {
if constexpr(is_pointer_like<outputIterator>::value) {
generateFastIntoBuffer(static_cast<typename It::pointer> (std::addressof(*outputIterator));
} else {
generateOneByOne(outputIterator);
}
}
这构成了使用元模板编程实现的功能的模板编程。
普通模板在哪里结束,元模板从哪里开始?
当模板生成的代码依赖于编程的基本方面(例如分支和循环)时,您已经从普通模板越过了模板元编程的界限。
按照您链接的文章的描述:
常规函数
bool greater(int a, int b)
{
return (a > b);
}
仅处理一种类型的常规函数(暂时忽略隐式转换)。
函数模板(泛型编程)
template <typename T>
bool greater(T a, T b)
{
return (a > b);
}
通过使用函数模板,您已经创建了可应用于许多类型的泛型代码。但是,根据其用法,对于以空值结尾的 C 字符串,它可能不正确。
模板元编程
// Generic implementation
template <typename T>
struct greater_helper
{
bool operator(T a, T b) const
{
return (a > b);
}
};
template <typename T>
bool greater(T a, T b)
{
return greater_helper<T>().(a > b);
}
// Specialization for char const*
template <>
struct greater_helper<char const*>
{
bool operator(char const* a, char const* b) const
{
return (strcmp(a, b) > 0);
}
};
在这里,你写了代码,好像在说:
如果T
char const*
,请使用特殊函数。
对于T
的所有其他值,请使用泛型函数。
现在,您已经跨越了普通模板到模板元编程的门槛。您已经引入了使用模板进行if-else分支的概念。
- 根据用户输入用字母填充矢量,并将"开始"和"结束"放在四肢
- 如何在 c++ 中确定一条指令(以字节为单位)在哪里结束,另一条指令从哪里开始?
- 如何显示函数开始、结束行和函数体?
- C++ 从具有开始位置和结束位置的列表中删除
- C++程序从主程序开始执行并在主程序结束?
- 如何使用Chrono或ctime libaray输入设置的开始和结束时间
- 是否可以在基于范围的 for 循环中使用模板化的开始/结束方法
- 重写自定义数组类的运算符/开始/结束
- 取代表开始/结束的数字对,并删除重叠
- 如何为一个类提供多个开始/结束代理
- 映射大小() > 0 但开始() == 结束()
- 将数组作为函数参数传递,并在其上调用开始/结束方法
- 正则表达式,捕获 4 个被 括起来的数字.或行开始/结束
- C++ 指针上的开始/结束 (arr) - 调用 'begin(int**&)' 没有匹配函数
- 重载指向集合的指针的开始/结束是否是个好主意
- 检测 D3D 挂钩中的帧开始/结束
- C ::P在循环开始结束时发出声音循环声音
- 三次样条:开始/结束段插值
- Std::regex,匹配字符串的开始/结束
- 匹配开始/结束分析调用