如何编码用于模板参数的类型列表?

How to encode a list of types to be used for template parameters?

考虑以下代码,我在其中定义了一个强类型枚举 Fruit,以及几个 PeelerX 类:

#include <vector>

enum class Fruit {
    Avocado,
    Banana,
    Coconut,
    Date,
    Elderberry
}

class Peeler1 {
public:
    Peeler1() {}
    ~Peeler1() {}
};

class Peeler2 {
public:
    Peeler2() {}
    ~Peeler2() {}
};

class Peeler3 {
public:
    Peeler3() {}
    ~Peeler3() {}
};

static const std::vector<Fruit> myFruits {
    Fruit::Coconut,
    Fruit::Avocado,
    Fruit::Banana,
    Fruit::Banana,
    Fruit::Elderberry,
    Fruit::Coconut,
    Fruit::Date,
    Fruit::Elderberry,
    Fruit::Date,
    Fruit::Avocado
}

static const std::vector<Type> myPeelers {
    // types?
    Peeler1,
    Peeler2,
    Peeler2,
    Peeler3,
    Peeler1,
    Peeler3,
    Peeler2,
    Peeler1,
    Peeler2
}

// Peeler function, takes peeler object as template parameter
template <typename T>
void peel(const Fruit& f) {
    // ...  
};


int main(int argc, char const *argv[]) {
    static_assert(myFruits.size() == myPeelers.size(), "");
    // ...
    size_t idx = 0;
    for (auto& fruit : myFruits) {
        peel< myPeelers[idx++] >(fruit);
    }
    // ...
    return 0;
}

有没有办法对编译时可访问的 myPeelers 类型列表进行编码,以用作模板参数?

此答案的第一部分仅展示了如何在编译时对类型序列进行编码并进行迭代,第 2 部分展示了如何在您的示例中使用它。

第 1 部分:模式选项 1

如果您在编译时需要一切,请查看选项 2,否则这就足够了。 您可以使用如下所示的变体:

using MyPeelers = std::variant<Peeler1, Peeler2, Peeler3>;

然后像这样使用矢量:

std::vector<MyPeelers> vec
{
    Peeler1{},
    Peeler2{},
    Peeler3{}
};

注意:std::vector 不是 "constexpr",它的元素不会在编译时添加。为此,您可以使用 std::array(参见 "Part 2 : Implementation Option 2")。

并像这样遍历元素:

for(auto & v: vec) 
{
    std::visit([](auto arg)
    { 
        std::cout << typeid(arg).name() << "\n"; 
    }, v);
}

您可以在这里尝试一下:https://onlinegdb.com/rJlqb1sVr

第 1 部分:模式选项 2

请注意,模式选项 2 可以在编译时完全评估(但如果您打印到控制台则不会...)。为此,您需要将 fruit 向量更改为包含 Fruit 元素的 constexpr std::array。

对于削皮器,您可以使用元组。此方法完全编译时。

constexpr std::tuple my_peelers{ Peeler1{}, Peeler2{}, Peeler3{} };

下面的方法使用折叠表达式(见https://en.cppreference.com/w/cpp/language/fold)。在这种情况下,逗号运算符。此折叠表达式分别遍历应用函数 lamba 的参数中的每个元素。每个参数的类型可以通过在 args 上使用 decltype 来确定。

std::apply([](auto ... args)
{
    ((std::cout << typeid(args).name() << "\n" ), ...);
}, 
my_peelers);

您可以在这里尝试一下:https://onlinegdb.com/BkbTgJiES

第 2 部分:实施选项 1

这里的第一个区别是 std::variant 涉及的地方:

using MyPeelers = std::variant<Peeler1, Peeler2, Peeler3>;
static const std::vector<MyPeelers> myPeelers 
{
    Peeler1{},
    Peeler2{},
    Peeler2{},
    Peeler3{},
    Peeler1{},
    Peeler3{},
    Peeler2{},
    Peeler1{},
    Peeler2{}
};

这个函数本质上没有变化,只是修改为打印模板参数和水果。

//
// Peeler function, takes peeler object as template parameter
template <typename T>
void peel(const Fruit& f) 
{
    std::cout << "Pealing  " << ToString(f) << " with " << T::Name << "\n";
}

按值遍历所有不同的削皮器是衰减类型的一种巧妙方法。如果您不使用 v 的值,则不应有与此方法相关的开销。 在循环内部使用 variant 和 he visitor 模式。将使用存储在 v 中的值调用 variants lambda 函数。调用后 "arg" 的类型是存储在 v 中的类型。此方法会为 variant 中的每个唯一类型生成一些代码。在本例中,Peeler1、Peeler2 和 Peeler3。我们可以通过在 "arg" 上使用 declval 来确定哪种类型。一旦我们有了类型,所需要的就是遍历水果。

    //
    // All by value, no values matter, only types
    for(auto v : myPeelers) 
    {
        std::visit
        (
            [](auto arg)
            { 
                for (auto fruit : myFruits) 
                {
                    peel<decltype(arg)>(fruit);
                }
            }, 
        v);
    }

看这里:https://onlinegdb.com/B1rf4Ji4H

以上程序的输出为:

Pealing  Coconut with Peeler1
Pealing  Avocado with Peeler1
Pealing  Banana with Peeler1
Pealing  Banana with Peeler1
Pealing  Elderberr with Peeler1
Pealing  Coconut with Peeler1
Pealing  Date with Peeler1
Pealing  Elderberr with Peeler1
Pealing  Date with Peeler1
Pealing  Avocado with Peeler1
Pealing  Coconut with Peeler2
Pealing  Avocado with Peeler2
Pealing  Banana with Peeler2
...

这样持续了一段时间...

第 2 部分:实施选项 2

这是我喜欢的实现方式,但是您不能在 运行 时更改 pealer 数组的大小,所以这可能很烦人。

这与以前的工作方式相同,除了两个地方:

pealers 现在被定义为一个元组:

static constexpr std::tuple myPeelers 
{
    Peeler1{},
    Peeler2{},
    Peeler2{},
    Peeler3{},
    Peeler1{},
    Peeler3{},
    Peeler2{},
    Peeler1{},
    Peeler2{}
};

为了确保一切都在编译时定义,fruit 现在使用 std::array:

static constexpr std::array myFruits 
{
    Fruit::Coconut,
    Fruit::Avocado,
    Fruit::Banana,
    Fruit::Banana,
    Fruit::Elderberry,
    Fruit::Coconut,
    Fruit::Date,
    Fruit::Elderberry,
    Fruit::Date,
    Fruit::Avocado
};

推导出元组的模板参数。我觉得这样比较方便。

迭代如"Part 1: Pattern Option 2"中所述。在 args 参数上使用 decltype 获取元组中每个元素的类型。它使用类型作为剥离的模板参数。

std::apply([](auto ... args)
{
    for (auto fruit : myFruits) 
    {
        (peel<decltype(args)>(fruit), ...);
    }
}, 
myPeelers);

您可以在此处运行:https://onlinegdb.com/B1W4wysEB 输出是一样的。

程序集显示所有内容似乎都在编译时求值(打印语句除外)。 https://godbolt.org/z/17u3v5

第 2 部分:解决方案选项 3 (C++11)

根据要求,我将其改编为 C++11 解决方案。

第一个区别是从所有函数中删除了 constexpr。

下一个区别是我不能再将值存储在元组中,元组已发布 C++17。但这需要成为一个问题,只需将其编码在一个函数中,您就可以获得相同的模板参数类型推导。

第三个区别是std::array不再方便,转换为Fruit[]:

static Fruit myFruits[]
{
    Fruit::Coconut,
    Fruit::Avocado,
    Fruit::Banana,
    Fruit::Banana,
    Fruit::Elderberry,
    Fruit::Coconut,
    Fruit::Date,
    Fruit::Elderberry,
    Fruit::Date,
    Fruit::Avocado
};

为了遍历每个 pealer,我们使用可变参数模板和尾端递归。为了停止递归,我们有一个空的终端案例。

这是终端案例:

void Pealers()
{
}

这是正常情况:

template <typename Front, typename ... Args>
void Pealers(Front, Args ... args)
{
    for (Fruit fruit : myFruits)
    {
        peel<Front>( fruit );
    }
    Pealers(args...);
}

要使用上面的方法,请执行以下操作:

Pealers
(
    Peeler1{},
    Peeler2{},
    Peeler2{},
    Peeler3{},
    Peeler1{},
    Peeler3{},
    Peeler2{},
    Peeler1{},
    Peeler2{}
);

这里的代码可以是运行:https://onlinegdb.com/BkKapR2NS 程序集可以在这里查看:https://godbolt.org/z/1psKDk

没有打印语句的版本在这里显示完整内联https://godbolt.org/z/-2DewW

竟然有办法。 注意:这适用于 C++14 及更高版本。

using Types = std::tuple
<
    Peeler1,
    Peeler2,
    Peeler2,
    Peeler3,
    Peeler1,
    Peeler3,
    Peeler2,
    Peeler1,
    Peeler2,
    Peeler1
>;

类型列表在 std::tuple 中 "stored"。

void peelFruits()
{
    _peelFruitsDetail_(std::make_index_sequence<std::tuple_size<Types>::value>{});
}

此函数的目的是使用 std::make_index_sequence for the detail function below. The indices are represented as a parameter pack and range from 0 to N - 1 (N here is the std::tuple_size).

生成索引
template<size_t... Indices>
void _peelFruitsDetail_(std::index_sequence<Indices...>)
{
    auto i = { (peel<std::tuple_element_t<Indices, Types>>(myFruits[Indices]), 0)... };
}

此处索引作为 std::tuple_element_t 的模板参数传递。有了这个,我们推断出类型列表的 N 处的类型。然后将推导的类型以及 fruits 列表元素的引用传递给 peel 函数。逗号运算符丢弃 peel 的 return 值和 returns 0。这是必要的,以便结果 std::initializers_list 填充值。 std::initializers_list 确保扩展包中的表达式以正确的顺序求值。该包扩展为如下所示:

auto i = { peel<std::tuple_element_t<0, Types>>(myFruits[0]), peel<std::tuple_element_t<1, Types>>(myFruits[1]), ..., peel<std::tuple_element_t<N - 1, Types>>(myFruits[N - 1]) };

注意:为了清楚起见,我省略了上面的逗号运算符。

因此下面的剥离函数将按顺序调用。

template<typename T>
void peel(const Fruit&)
{
}

另外值得一提的是,您的代码中的 static_assert 无法编译,因为向量不是 constexpr 类型。

编辑

为了在 C++11 中实现这一点,我们需要做以下事情:

  1. 制作我们自己的索引生成器。
  2. std::tuple_element_tstd::tuple_size_v 替换为 std::tuple_elementstd::tuple_size

std::make_index_sequence 使用 indices trick。看起来像这样:

template <std::size_t... Is>
struct indices {};

template <std::size_t N, std::size_t... Is>
struct build_indices
    : build_indices<N - 1, N - 1, Is...> {};

template <std::size_t... Is>
struct build_indices<0, Is...> : indices<Is...> {};

这里发生了什么?

当我们调用 build_indices(相当于 std::make_index_sequence)时,它开始递归地继承自身。它从右到左填充自己,直到 N 接近 0。在 0 时它继承 indices 以便可变参数模板参数 Is 得到 "stored".

此外,上面的 indices 结构相当于 std::index_sequence.

以上所有内容使代码看起来像这样:

template<size_t... Indices>
void _peelFruitsDetail_(indices<Indices...>)
{
    auto i = { (peel<typename std::tuple_element<Indices, Types>::type>(myFruits[Indices]), 0)... };
}

void peelFruits()
{
    _peelFruitsDetail_(build_indices<std::tuple_size<Types>::value>{});
}