【问题标题】:How to encode a list of types to be used for template parameters?如何编码用于模板参数的类型列表?
【发布时间】:2019-08-21 14:58:10
【问题描述】:

考虑以下代码,我在其中定义了一个强类型枚举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 上的类型列表进行编码,可以用作模板参数?

【问题讨论】:

  • 类型信息最好用其他类型编码。例如,std::tuple&lt;Peeler1, Peeler2, ..., PeelerN&gt;。但它并不容易使用。而且它不适用于像 idx 这样的运行时值。
  • 看起来您需要std::variant 并使用访问者模式。
  • 或者只是一个带有模板类型列表的空结构“标签”
  • 根据您想对Peelers 执行的操作,还有其他解决方案。我不清楚你是想实例化它们还是将它们用作函数对象。
  • 您仍然可以围绕具体类型提供模板包装器,例如template&lt;&gt; class MyPeeler&lt;Fruit::Date&gt; { PeelerX m_peeler };。这种方法与上述模板函数一起,可以防止为去皮器列表中的一个水果放置一个坏的去皮器(例如对香蕉使用一次 Peeler1,然后对苹果使用一次 Peeler1)。

标签: c++ templates compile-time


【解决方案1】:

这个答案的第一部分只是展示了如何在编译时对一系列类型进行编码和迭代,第二部分展示了如何在你的例子中使用它。

第 1 部分:模式选项 1

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

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

然后像这样使用向量:

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

注意:std::vector 不是“constexpr”,它的元素不会在编译时添加。为此,您可以使用 std::array (参见“第 2 部分:实施选项 2”)。

然后像这样遍历元素:

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

你可以在这里玩这个:https://onlinegdb.com/rJlqb1sVr

第 1 部分:模式选项 2

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

对于剥离器,您可以使用元组。这个方法是完全编译时的。

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

以下方法使用折叠表达式(参见https://en.cppreference.com/w/cpp/language/fold)。在这种情况下,超过逗号运算符。此折叠表达式分别遍历应用函数 lba 的参数中的每个元素。每个参数的类型可以通过在 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 的值,则不应有与此方法相关的开销。 在循环内部使用变量和访问者模式。将使用存储在 v 中的值调用 variables 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

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

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

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

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

迭代如“第 1 部分:模式选项 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
};

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

这是终端案例:

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

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

【讨论】:

  • 不错,干净的解决方案,两者都是。但是有没有办法做到这一点 a) 使用 C++11 和 b) 考虑 Peeler 类/函数不是 constexpr 的情况?
  • 添加了一个 c++ 11 版本(第 2 部分:解决方案 3)。
  • 只是一个小观察,std::tuple 实际上是从std::c++11 开始可用的
  • 是的,但 std::apply 和 std::visit 不是。这就是为什么我用另一种方式来做。
【解决方案2】:

令人惊讶的是有一种方法。 注意:这适用于 C++14 及更高版本。

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

类型列表“存储”在std::tuple 中。

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

此函数的目的是为下面的详细函数生成带有std::make_index_sequence 的索引。索引表示为 parameter pack,范围从 0 到 N - 1(这里的 N 是 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 处的类型。然后将推导的类型以及水果列表元素的引用传递给剥离函数。逗号运算符丢弃了剥离的返回值并返回 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 被“存储”。

上面的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>{});
}

【讨论】:

  • 我更喜欢我的 tuple 方法,但很高兴看到替代方法 :) 我想知道我们的 tuple 方法是否编译为同一个程序集?
  • @DavidLedger 您的怀疑是correct(与this 相比)。我也更喜欢你的方法,因为我觉得它更具可读性。
  • 我假设这仅适用于 PeelerX 类是 constexpr/literal 类型?另外,C++11 的任何替代方案?
  • @joaocandre PeelerX 类作为数据类型存储在 Types 元组中。这意味着没有构造 PeelerX 类(您可以在 peel 函数中将它们构造为 constexpr 或其他内容)。有关 C++11 支持,请参阅编辑。
猜你喜欢
  • 1970-01-01
  • 2019-09-03
  • 1970-01-01
  • 2011-07-26
  • 2015-10-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-06
相关资源
最近更新 更多