【问题标题】:Where do normal templates end and meta templates begin?普通模板从哪里结束,元模板从哪里开始?
【发布时间】:2017-04-08 04:12:26
【问题描述】:

Jörg's answerthis 问题很好地描述了对数据和元模板进行操作的“正常”模板(问题所指的,可能是错误的泛型)和对程序进行操作的元模板。 Jörg 然后明智地提到程序 数据,所以它真的都是一回事。也就是说,元模板仍然是另一种野兽。 普通模板在哪里结束,元模板从哪里开始?

我能想到的最好的测试是如果模板的参数完全是classtypename,则模板是“正常的”,否则是元。这个测试正确吗?

【问题讨论】:

  • 那么std::array<typename T, size_t n>呢?是普通的还是元的?我会说模板在编译时以某种方式使用但从未实例化时是“元”。
  • @HenriMenke,我的测试会说 meta,因为参数不只是 classtypename。我不知道我的测试是否正确;)

标签: c++ templates template-meta-programming


【解决方案1】:

边界:带有逻辑行为的签名

好吧,在我看来,边界线将被绘制在模板签名不再是产生运行时代码的简单签名并成为显式或隐式逻辑的定义处,该定义将在以下位置执行/解决编译时。

一些例子和解释

常规模板,即只有类型名、类或可能的值类型模板参数,一旦在编译时实例化,就会产生可执行的 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 类型的代码,但不会执行。 另外,specialization-template 会为 text-concatenation 实例化,而且:不会在编译时执行。

然而,这个例子:

#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”函数有一个模板化参数,如果传递给它的类型是从 INPCWithVoice 派生的,该参数在编译时解析并衰减到 TNPC。

这反过来意味着,如果没有,模板将没有实例化的候选对象,并且编译已经失败。 查找 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 上有一个模板元编程功夫大师,他可以补充任何缺少的必要信息。

【讨论】:

  • 请避免所有大写锁定词。看起来你在对我们大喊大叫!!!如果你想强调使用粗体或斜体,但也不要过度使用。
  • 我不明白你的意思。有很多文字和代码,我觉得它没有传达你的观点(至少对我来说)。
  • @bolov 感谢您的评论,已修复。是的,有很多解释和例子来强调“现在边界在哪里”中的观点......
【解决方案2】:

尝试区分和定义术语

让我们首先尝试粗略地定义这些术语。我从一个希望足够好的“编程”定义开始,然后反复应用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++ 中,编写代码会产生新的模板。 (来自another answer of me:)

// 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 的类型(因此赋予行为,由该类型实现的程序)。

【讨论】:

  • 我喜欢你的解释,“正常”模板是元编程,而“元模板”是元元编程。
【解决方案3】:

让我开始使用dictionary.com 中的定义来回答

定义

元 -

  1. 添加到主题名称的前缀并指定另一个主题,该主题分析原始主题,但在更抽象,更高的层次上:元哲学;元语言学。

  2. 加在某事物名称上的前缀,有意识地引用或涉及其自身的主题或特征: 艺术家在画布。

模板编程最常被用作在 C++ 类型系统中表达关系的一种方式。因此,我认为可以公平地说模板编程天生就利用了类型系统本身。

从这个角度来看,我们可以直接应用上面给出的定义。模板编程和元(模板)编程的区别在于模板参数的处理和预期的结果。

检查其参数的模板代码显然属于前一种定义,而从模板参数创建新类型则属于后者。请注意,这还必须与您的代码对类型进行操作的意图相结合。

示例

让我们看一些例子:

std::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 有专门的类型名参数,但很明显是元编程。如果你深入挖掘,你会发现类型系统本身,带有模板,是图灵完备的。甚至不需要明确说明任何整数变量,例如,我们可以通过递归堆叠模板轻松替换它们(即自然数的 Zermelo 构造)

using Z = void;
template<typename> struct Zermelo;
template<typename N> using Successor = Zermelo<N>;

在我看来,更好的测试是询问给定的实现是否具有运行时效果。如果模板结构或别名不包含任何仅在运行时产生效果的定义,则可能是模板元编程。

结束语

当然,普通的模板编程可能会使用元模板编程。您可以使用元模板编程来确定普通模板参数的属性。

例如,您可能会选择不同的输出策略(假设 template&lt;class Iterator&gt; 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);
    }
}

这构成了使用元模板编程实现的功能的模板编程。

【讨论】:

    【解决方案4】:

    普通模板在哪里结束,元模板从哪里开始?

    当模板生成的代码依赖于编程的基本方面(例如分支和循环)时,您已经跨越了从普通模板到模板元编程的界限。

    按照您链接的文章的描述:

    常规函数

    bool greater(int a, int b)
    {
       return (a > b);
    }
    

    仅适用于一种类型的常规函数​​(暂时忽略隐式转换)。

    函数模板(泛型编程)

    template <typename T>
    bool greater(T a, T b)
    {
       return (a > b);
    }
    

    通过使用函数模板,您创建了可应用于多种类型的通用代码。但是,根据其用法,它可能不适用于以 null 结尾的 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);
       }
    };
    

    这里,你写的代码好像在说:

    如果Tchar const*,请使用特殊函数。
    对于T 的所有其他值,请使用通用函数。

    现在您已经跨过了普通模板的门槛,进入了模板元编程。您已经介绍了 if-else 使用模板进行分支的概念。

    【讨论】:

    • 这是否意味着不能仅通过查看模板的签名来判断模板是否为元?从您所说的看来,您必须查看周围的模板才能确定。
    • @chessofnerd,如果模板的行为被清楚地记录下来,它可能会提供关于它是简单模板还是使用模板元编程的线索。查看它的实现以及它所依赖的实现肯定会更有启发性。
    • 我喜欢你的回答简洁明了;但是,我仍然有点困惑。如果我的测试是正确的,你能给我一个明确的是 - 否吗?如果没有,可以修改吗?
    • @chessofnerd,答案是“不”。
    猜你喜欢
    • 2010-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多