【问题标题】:How to order types at compile-time?如何在编译时订购类型?
【发布时间】:2018-02-10 18:09:00
【问题描述】:

考虑以下程序:

#include <tuple>
#include <vector>
#include <iostream>
#include <type_traits>

template <class T>
struct ordered {};

template <class... T>
struct ordered<std::tuple<T...>>
{
    using type = /* a reordered tuple */;
};

template <class T>
using ordered_t = typename ordered<T>::type;

int main(int argc, char* argv[])
{
    using type1 = std::tuple<char, std::vector<int>, double>;
    using type2 = std::tuple<std::vector<int>, double, char>;
    std::cout << std::is_same_v<type1, type2> << "\n"; // 0
    std::cout << std::is_same_v<ordered_t<type1>, ordered_t<type2>> << "\n"; // 1
    return 0;
}

ordered 帮助器必须对元组中的类型重新排序,以便具有相同类型但排序不同的两个元组导致相同的元组类型:可以是第一个,第二个,甚至是另一个:它只需要具有相同的大小和相同的元素,但具有唯一的顺序(无论此顺序如何)。

是否可以在编译时使用模板元编程技术做到这一点?

【问题讨论】:

  • 有可能吗?是的。无论你得到什么答案,Andrei Alexandrescu 早在 2001 年就曾写过关于在“现代 C++ 设计”中做这些事情的文章。细节无疑会有所不同,但核心思想是相同的。
  • @StoryTeller:如果T != T2sizeof(T1) == sizeof(T2) 会怎样?您将如何唯一地对它们进行排序,以便 std::is_same 按预期工作?
  • @Nawaz - 对任何其他类型的数据执行相同的操作。如果库不能,则强制用户提供任意顺序。这并非不可能。可以像添加一个或两个模板专业化一样简单。
  • @StoryTeller:这将算法变成了一个怪物,因为很难为每个大小相等的这样的对提供模板专业化等。那么为什么问题会是,为什么要使用这种排序 metafn 开始呢?为什么不手动执行此操作?似乎这方面缺乏 C++。应该有一种方法可以在编译时对类型进行排序.. 一个编译时运算符,相当于 std::less&lt;&gt; 用于类型等。
  • @Vincent:这整个问题有可能是 XY 问题吗?如果您只需要比较两个元组的等效性,那么可能不需要对它们进行排序 - 使用类型别名技术进行搜索,即使 n^2 可能比对两个集合进行排序并比较它们更快(好吧,我不会赌一把,但值得一试)。

标签: c++ types tuples template-meta-programming c++17


【解决方案1】:

困难的部分是想出一种订购类型的方法。按谓词对类型列表进行排序是一件苦差事,但也是可行的。我将在这里只关注比较谓词。

一种方法是创建一个类模板,为每种类型定义一个唯一的 id。这很有效,并且可以轻松编写比较器:

template <typename T, typename U>
constexpr bool cmp() { return unique_id_v<T> < unique_id_v<U>; }

但是想出这些独特的 ID 是一个不一定可行的障碍。您是否将它们全部注册在一个文件中?这不能很好地扩展。

如果我们可以...得到所有类型的名称作为编译时字符串,那就太好了。反思会给我们这个,然后这个问题是微不足道的。在那之前,我们可以做一些更脏的事情:使用__PRETTY_FUNCTION__。 gcc 和 clang 都可以在 constexpr 上下文中使用该宏,尽管它们对该字符串有不同的格式。如果我们有这样的签名:

template <typename T, typename U>
constexpr bool cmp();

然后 gcc 将 cmp&lt;char, int&gt; 报告为 "constexpr bool cmp() [with T = char; U = int]" 而 clang 将其报告为 "bool cmp() [T = char, U = int]"。它是不同的......但足够接近我们可以使用相同的算法。基本上是:找出TU 的位置,然后进行正常的字符串字典比较:

constexpr size_t cstrlen(const char* p) {
    size_t len = 0;
    while (*p) {
        ++len;
        ++p;
    }
    return len;
}

template <typename T, typename U>
constexpr bool cmp() {
    const char* pf = __PRETTY_FUNCTION__;
    const char* a = pf + 
#ifdef __clang__
        cstrlen("bool cmp() [T = ")
#else
        cstrlen("constexpr bool cmp() [with T = ")
#endif
        ;

    const char* b = a + 1;
#ifdef __clang__
    while (*b != ',') ++b;
#else
    while (*b != ';') ++b;
#endif
    size_t a_len = b - a;
    b += cstrlen("; U = ");
    const char* end = b + 1;
    while (*end != ']') ++end;
    size_t b_len = end - b;    

    for (size_t i = 0; i < std::min(a_len, b_len); ++i) {
        if (a[i] != b[i]) return a[i] < b[i];
    }

    return a_len < b_len;
}

通过一些测试:

static_assert(cmp<char, int>());
static_assert(!cmp<int, char>());
static_assert(!cmp<int, int>());
static_assert(!cmp<char, char>());
static_assert(cmp<int, std::vector<int>>());

这不是最漂亮的实现,而且我不确定它是否受到标准的有意义的认可,但它可以让您编写您的排序,而无需手动和仔细地注册所有类型。它在clanggcc 上编译。所以也许它已经足够好了。

【讨论】:

  • boost::hana 中,类型的表示是相等可比static_assert( boost::hana::type_c&lt;char&gt; == boost::hana::type_c&lt;char&gt; );,但不幸的是不小于可比因为类型不是Orderable。这将是对 Hana 的一个很好的补充,使它们可订购,一个潜在的陷阱是订单最终可能依赖于平台/编译器。
  • 鉴于template&lt;auto&gt; S{};,您对cmp&lt;S&lt;42&gt;, S&lt;42u&gt;&gt;() 有什么想法吗? Clang 和 gcc 在签名中都没有 auto 参数的类型。顺便说一句,这是一个更完整的实现godbolt.org/z/My3ktR
  • @PasserBy 呃……这很不幸。 typeid 将两者区分开来,但这在这里并没有真正的帮助。
【解决方案2】:

这是对 Barry 提出的方法的轻微修改,适用于 Visual Studio。而不是创建存储函数名称的编译时字符串:

template <typename T, typename U>
constexpr bool cmp() 

此方法直接比较 type_name::name() 返回的两种类型的名称。当宏 __PRETTY_FUNCTION__ 返回的类型 T 和 U 的名称用逗号分隔时,Barry 的方法不起作用,因为当 T 或 U 是类或函数模板时,逗号也可以分隔模板参数。

// length of null-terminated string
constexpr size_t cstrlen(const char* p) 
{
    size_t len = 0;
    while (*p) 
    {
        ++len;
        ++p;
    }

    return len;
}

// constexpr string representing type name
template<class T>
struct type_name
{
    static constexpr const char* name() 
    { 
        #if defined (_MSC_VER)
            return __FUNCSIG__; 
        #else
            return __PRETTY_FUNCTION__;
        #endif
    };
};

// comparison of types based on type names
template<class T1, class T2>
constexpr bool less()
{
    const char* A   = type_name<T1>::name();
    const char* B   = type_name<T2>::name();

    size_t a_len    = cstrlen(A);
    size_t b_len    = cstrlen(B);

    size_t ab_len   = (a_len < b_len) ? a_len : b_len;

    for (size_t i = 0; i < ab_len; ++i) 
    {
        if (A[i] != B[i]) 
            return A[i] < B[i];
    }

    return a_len < b_len;
}

// simple checks
template<class ... Type>
struct list;

static_assert(less<list<int, void, list<void, int>>, list<int, void, list<void, void>>>());
static_assert(less<list<int, void, list<void, int>>, list<int, void, list<void, int>, int>>());

此方法适用于 VS。我不确定它是否适用于 Clang 或 GCC。

【讨论】:

  • 它实际上在 Clang 和 GCC 上工作,除了一个小错误(name() 需要返回一个 const char *:我已经提交了对您帖子的编辑)。在 Godbolt 上观看直播:godbolt.org/z/6FwH7x
【解决方案3】:

tl;dr:在编译时获取类型名称,并按此排序。

在我看来,以前的答案有点特殊——至少在实施中是这样。

此时,我们有一个非常好的、支持多编译器的function,用于获取类型名称作为编译时字符串,作为字符串视图。我只会在这里引用它的签名:

template <typename T>
constexpr std::string_view type_name();

这构成了从类型到编译时可比较值的单射映射。鉴于这些,您可以轻松实现类似selection-sort 的过程来获取每种类型的相对顺序。最后,您使用这些顺序组装一个新元组。

【讨论】:

  • 这个答案对我很有用,但我想我是唯一一个赞成的 atm,因为它不包含完整的工作代码......但是是的,你解决了我的问题(不同的是我没有费心去修剪名称,因为我不在乎内容是什么,只要它能让我订购类型)。
  • @NoSenseEtAl:修正了一个错字并添加了缺失的包含。现在似乎可以编译了。
  • 看到这个working on GodBolt
  • 不错,挑剔:也将 msvc 添加到链接。人们不会看到输出,因为它不受支持,但很高兴看到它在 Godbolt 上使用最新的 msvc 编译。
  • @NoSenseEtAl:这实际上在关于确定类型名称的问题页面上比在此处更有意义。所以我实际上只是决定删除整个东西。另外,在那个页面上,我建议了一个更健壮(虽然更长)的实现。
猜你喜欢
  • 2015-04-15
  • 1970-01-01
  • 2012-06-10
  • 2018-03-29
  • 2019-10-03
  • 2018-01-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多