如何通过未定义的类型定义元函数

How to define metafunctions by undefined types?

本文关键字:定义 函数 类型 何通过 未定义      更新时间:2023-10-16

请考虑像

这样的元函数
#include <type_traits>
template <typename T, T N, T M>
struct Sum : std::integral_constant <T, N + M> {};
template <typename T, T N, T M>
struct Product : std::integral_constant <T, N * M> {};

它们的结果可以通过::value成员提取:

static_assert (Sum <int, 3, 4>::value == 7, "3 + 4 == 7");
static_assert (Product <int, 2, 5>::value == 10, "2 * 5 == 10");

两个元函数都有类似的静态签名。也就是说,它们将T与每一对T相关联,其中T受到与std::integral_constant施加的限制相同的限制,并且可以是可和的或可乘的。因此,我们可以创建一个泛型元函数来执行计算。

template <typename T, template <typename U, U, U> class F, T N, T M>
struct EvaluateBinaryOperator : std::integral_constant <T, F <T, N, M>::value> {};
static_assert (EvaluateBinaryOperator <int, Sum, 3, 4>::value == 7, "3 + 4 == 7");
static_assert (EvaluateBinaryOperator <int, Product, 2, 5>::value == 10, "2 * 5 == 10");

当单独以这种形式使用时,用std::integral_constant的结构污染SumProduct感觉是多余的。为了表明我们确实可以不需要,请考虑以下事项:

template <typename T, T N, T M, T R = N + M>
struct Sum;
template <typename T, T N, T M, T R = N * M>
struct Product;
template <typename> struct EvaluateBinaryOperator;
template <typename T, template <typename U, U, U, U> class F, T N, T M, T R>
struct EvaluateBinaryOperator <F <T, N, M, R> > : std::integral_constant <T, R> {};
static_assert (EvaluateBinaryOperator <Sum <int, 3, 4> >::value == 7, "3 + 4 == 7");
static_assert (EvaluateBinaryOperator <Product <int, 2, 5> >::value == 10, "2 * 5 == 10");

不使用SumProduct的成员,我们专门化一个默认实参,并只在EvaluateBinaryOperator中提取它。作为一个额外的好处,SumProduct可以保持没有定义,使它们变得非常不可推断和不可构造,语法看起来也干净得多。现在,问题来了。如果我们希望所有的元函数都有一个统一的静态接口呢?也就是说,如果我们引入

template <typename...> struct Tuple;
template <typename T, T> struct Value;

并要求我们所有的元函数看起来像template <typename> struct ?例如,

template <typename> struct Sum;
template <typename T, T N, T M>
struct Sum <Tuple <Value <T, N>, Value <T, M> > > : 
    std::integral_constant <T, N + M> {};
template <typename> struct Product;
template <typename T, T N, T M>
struct Product <Tuple <Value <T, N>, Value <T, M> > > : 
    std::integral_constant <T, N * M> {};

现在,我们想把它们转换成:

template <typename, typename> struct Sum;
template <typename T, T N, T M, typename R = Tuple <Value <T, N + M> > >
struct Sum <Tuple <Value <T, N>, Value <T, M> >, R>;
template <typename, typename> struct Product;
template <typename T, T N, T M, typename R = Tuple <Value <T, N * M> > >
struct Product <Tuple <Value <T, N>, Value <T, M> >, R>;

使得我们可以用

提取值
template <typename> struct Evaluate;
template <template <typename, typename> class F, typename I, typename O>
struct Evaluate <F <I, O> > {
    typedef O Type;
};
static_assert (std::is_same <
    Evaluate <Sum <Tuple <Value <int, 3>, Value <int, 4> > > >::Type,
    Tuple <Value <int, 7> >
>::value, "3 + 4 == 7");
static_assert (std::is_same <
    Evaluate <Product <Tuple <Value <int, 2>, Value <int, 5> > > >::Type,
    Tuple <Value <int, 10> >
>::value, "2 * 5 == 10");

熟悉c++标准的人会立即指出14.5.5/8:"专门化的模板形参列表不应包含默认的模板实参值。",并附有戏弄的脚注:"它们不可能被使用。"实际上,给任何现代编译器提供这段代码都会在SumProduct模板专门化上产生关于违反标准的编译器错误。除了证明上述脚注缺乏作者的想象力;我们已经为它们创建了一个有效的用例。

我的问题现在可以提出:是否有其他方法可以实现类似的效果,其中SumProduct仍然是未定义/不完整类型,从而平凡地不可推断和不可构造,同时仍然承担执行操作的责任?欢迎提出任何建议。

元编程是复杂的。这并不是语言的设计特征,而是人们发现的。因此,很难做到正确——因此人们提出了如何调用元函数的约定,以便为其他程序员提供理解代码的指南。最重要的约定之一是,要获得元函数的结果,您可以这样做:

typename metafunc<some_args...>::type

您关于编写Sum的各种建议根本不符合这个惯例,我认为即使是许多非常有经验的模板元程序员也很难跟上您的做法——甚至依赖于对模板部分专门化如何工作的规则的更改。然而,改变这些规则根本不是一个令人信服的例子。让我提出更好的建议。

在元编程中,

类型是一等公民。一切都与类型有关。值和模板模板则不是。它们充其量只是笨拙。您需要编写Sum<int, 1, 2>的事实很糟糕。此外,当您必须允许具有不同数量参数的值或模板模板时,基本上不可能编写泛型元函数。让我们试着把所有东西都归类。

是Boost。MPL使用的是一个元函数类。我们可以对它进行一点c++ 11化,并将元函数类定义为如下的类:

struct C {
    template <typename... Args> // doesn't have to be variadic
    using apply = /* whatever */;
};

使用这个想法,我们可以说EvaluateBinaryOperator看起来像:

template <typename Op, typename A1, typename A2>
struct EvaluateBinaryOperator {
    using type = typename Op::template apply<A1, A2>;
};

请注意,当一切都是类型时,它是多么干净!当然,调用apply的语法并不像steller那样复杂,但这非常简单。事实上,我们可以归纳为:

template <typename Op, typename... Args>
struct EvaluateOperator {
    using type = typename Op::template apply<Args...>;
};

容易。现在让我们回到Sum。类型是一等公民,所以它不再接受值。它需要类型。但是我们仍然可以强制这些类型是具有相同类型的整型常量:

class Sum {
    template <typename, typename >
    struct impl;
    template <typename T, T a, T b>
    struct impl<std::integral_constant<T, a>,
                std::integral_constant<T, b>>
    {
        using type = std::integral_constant<T, a+b>;
    };
public:
    template <typename T, typename U>
    using apply = typename impl<T, U>::type;
};

这符合元函数类的模型,所以我们可以在EvaluateOperator中使用它,例如:

std::cout << EvaluateOperator<Sum, 
                    std::integral_constant<int, 1>,
                    std::integral_constant<int, 2>
                    >::type::value << std::endl; // prints 3

用两个不是相同底层类型的整型常量的类型实例化impl会给你想要的不完整类型错误。

使用元函数类还为您提供了能够进行curry的优势。你不能真正地从元函数中"返回"一个类模板,但是你可以返回一个元函数类:

template <typename Op, typename... Args>
struct curry {
    struct type {
        template <typename... OtherArgs>
        using apply = typename EvaluateOperator<Op, Args..., OtherArgs...>::type;
    };
};
using Add1 = curry<Sum, std::integral_constant<int, 1>>::type;
std::cout << Add1::apply<
                 std::integral_constant<int, 5>
             >::type::value << std::endl; // prints 6