抽象化非类型模板参数的类型

6
我希望编写一个模板,可以将类型拆解成具有非类型模板参数的模板,并携带其非类型模板参数。例如,它将把Array<5>拆分成template<int> Array5,但可以通用于任何类型的非类型模板参数(整数类型、指针、成员指针等)。
首先尝试使用模板特化:
template<typename T> struct foo { enum { n = 1 }; };

template<int x> struct bar { enum { n = x }; };

template<typename T, template<T> class X, T x>
struct foo< X<x> > { enum { n = x }; }; // here x must be of integral type, but that's just for testing

int main(int, char**) { return foo< bar<16> >::n; }

Clang 3.1 表示:

test145.cpp:6:8: warning: class template partial specialization contains a template parameter that can not be deduced; this partial specialization will never be used
struct foo< X<x> > { enum { n = x }; };
       ^~~~~~~~~~~
test145.cpp:5:19: note: non-deducible template parameter 'T'                     
template<typename T, template<T> class X, T x>
                  ^
1 warning generated.

第二次尝试,使用函数模板:

template<typename T, T x> 
struct box 
{ 
    static constexpr T value() { return x; }
};

template<typename T, template<T> class X, T x>
box<T, x> foo(X<x>);

template<int> struct asdf { };

int main(int, char**) { return decltype(foo(*(asdf<9>*)0))::value(); }

Clang 说:

test150.cpp:12:41: error: no matching function for call to 'foo'
int main(int, char**) { return decltype(foo(*(asdf<9>*)0))::value(); }
                                        ^~~
test150.cpp:8:11: note: candidate template ignored: couldn't infer template argument 'T'
box<T, x> foo(X<x>);
          ^
1 error generated.

GCC 4.7也有类似的说法。

这是一种基本限制吗?

奖励问题:如果是,那么是否有任何方法可以在有限的代码量内处理所有无限的可能性,即使这需要更少简单和通用的代码?(对于指针来说这很困难:由于同样的原因,似乎您不能编写 template<T>,我认为您也不能编写 template<T*>。)

请不要问为什么我问。


1
我必须说:我不明白你需要什么。 - BЈовић
2
我相信可以这样总结:让 template<int> struct A {}。是否可能(如果可能,如何)编写一个模板 arg,使得 arg<Array<5> >::template_template<int> Arrayarg<Array<5> >::typeintarg<Array<5> >::value5(且类型为 int),并且使其通用化,以便以这种方式处理每个可能的非类型模板参数。 - reima
基本上就是这样了。谢谢你比我更清楚地表达出来! - glaebhoerl
2个回答

3

答案可能会有点晚...

错过的尝试...

(请参见下面的正确答案C++17解决方案)


这篇原始答案被保留作为我在SO上的第一个回答的纪念。
不算是失败,可以说是第一次尝试失败了... ;)
现在,跳到下一行... 当我在寻找相关问题的答案时,我偶然遇到了这个问题。读完后,我告诉自己:"嗯...我已经做过这件事了。而且它起作用了。我当时怎么做的?!"。然后,我继续寻找我的问题的答案...
今天我觉得应该花点时间提出这个问题的解决方案(实际上是两个)
正如您已经注意到的那样,问题在于编译器不知道如何推断T。人们可以将错误消息解释为"请给我一点关于T的帮助"
我做的第一个工作版本是具有类似于std::integral_constant的类的foo专业化。让foo派生自std::integral_constant可能有助于编译器找出T的类型。(或者也许MSVC-vs2019-对我友好了一点) 无论如何,与此同时,我找到了更好的解决方案。编译器不应该无法推断出T的类型,因为x的类型不需要typename T参数...

这里是:(C++17 解决方案)

template<typename T> struct foo {};

template<auto x, template<decltype(x)> class X>
struct foo<X<x>> {
    using            arg_type   = decltype(x);
    static constexpr arg_type n = x;
};

//template<int x> struct bar { enum { n = x }; };
template<int x> struct bar;

using bar_16            = foo<bar<16>>;
using bar_16_arg_t      = typename bar_16::arg_type; // int
constexpr auto bar_16_n = bar_16::n;                 // 16

请注意,为了使此方法起作用,甚至不需要bar是一个完整的类型。仅仅一个前向声明(就像这个例子中)就足够进行分解操作。
祝愉快...

正确答案

° 注意事项

  • 这是对9年前问题的回答并且
  • 此处提出的解决方案仅使用C++11功能
  • 此解决方案仅管理整数类型
    (其他类型留给读者练习)

    如果您的编译器支持C++17功能,则应首选上面发布的解决方案,因为它可以管理不仅仅是整数类型

只对工作代码示例感兴趣?
跳转至: "工作解决方案"

° 前言

在我的研究中,似乎直到现在为止,这个特定问题的解决方案还没有被发现(或者没有公开)。我认为我应该更详细地说明一下“为什么”和“如何”。希望这会得到赞赏,但总体来说:很有用...
我目前正在编写一个元编程库,其中包含许多编译时工具和功能。我希望能够尽快在GitHub上发布它。(谁知道呢)-无论如何...
当我意识到我的第一个答案只有在C++17之后才正确时,我的挫败感是无与伦比的...-可以说并不“准时”...:P
我心里想着,使用我现在所拥有的所有“仅编译时”的功能,我感觉9年前就应该有一种方法可以做到。
我开始思考如何使用仅C++11功能来完成此操作,大约一个小时后,我找到了一个可行的解决方案。(事实上有两个)
花费了我更多的时间来使其成为一个可用的解决方案。(事实上有两个)
写这篇文章花费了相当长的时间...:D
毕竟,可能有编译器“只好”理解仅限于C++11...:P

显然,由于当时可用的功能集更窄,所以找到的解决方案会更加详细...:D

° 搜索过程

首先,必须记住,当编译器输出 "无法推断" 时...
- 这并不意味着有错误(虽然可能有错误)。
- 它实际上意味着编译器没有人们想象的那么聪明。
- 这意味着需要给编译器提供帮助,以便它能够完成其工作...

清楚了吗?
- 编译器在友好地要求您完成其工作的一部分。
- 并且您有很大的机会:

  • 最终自己完成大部分工作...:P

在这里,编译器说“无法推断出T的类型”。
确实,在用作foo专业化的参数表达式中未使用T,因此无法从中推断出T...

首先,需要做一些事情来表示 typename T 和类型为 T 的值 x 之间的关系。想到的是需要一个类似于 std::integral_constant 的模板,它可以准确地实现这个功能。它将一个值和其对应的类型编码成一个新类型

免责声明 [!警告!]

  • 那些容易因标识符名称中的大写字母而产生过敏反应的人不应继续阅读本文!

到此为止还没有什么新鲜的内容?
太好了!这就是它:

template<typename T, T V>
struct NonTypeParam { using Type = T; static constexpr T Value = V; };

下一个需要的是能够创建具有值和相应类型的NonTypeParam模板实例的东西...
  • 可能是一个带有类型参数的模板。
  • 这个参数将接收要分解的类型。
  • 然后,必须以某种方式进行特化...
让我们试一试,并从以下内容开始:
template<typename T> struct Extract { using Result = void; };

为了完全抽象出Extract模板的专业化,你需要写类似以下内容:
template<typename T, T V, template<T> class C>
struct Extract<C<V>> { using Result = NonTypeParam<T, V>; };

这导致了同样的问题,因为这是在问题中使用的相同类型的专业化。此时,必须提醒编译器无法做什么。它“无法推断”参数T在我们的专业化中应该别名什么类型...
实际上,这条消息有点误导人,因为T甚至不是作为参数传递给专业化的一部分。因此,问题不在于将typename归属于参数T,而在于将类型归属于参数V...现在,应该能够提出正确的问题:
1. 如何从等式中删除T? - 通过明确定义V的类型。
2. V的可能类型是什么? - 允许作为非类型模板参数的类型。
首先要做的是,例如,如何通过显式定义V的类型来查看char的专业化呢?它看起来像这样:
template<char V, template<char> class C>
struct Extract<C<V>> { using Result = NonTypeParam<char, V>; };

这有点让人烦恼,但由于可能性有限,我们可以找到一种方法来减少后续的声明。让我们再添加一个专门的模板——受害者模板,并进行测试...

template<typename T, T V>
struct NonTypeParam { using Type = T; static constexpr T Value = V; };

template<typename T> struct Extract { using Result = void; };

template<char V, template<char> class C>
struct Extract<C<V>> { using Result = NonTypeParam<char, V>; };

template<std::size_t V, template<std::size_t> class C>
struct Extract<C<V>> { using Result = NonTypeParam<std::size_t, V>; };

template<std::size_t I> struct TestNonType1 {};

using Result          = typename Extract<TestNonType1<42>>::Result;
using RType           = typename Result::Type; // std::size_t
constexpr auto rValue = Result::Value;         // 42

没有意外,它按预期工作...
... 现在可能有哪些类型?
根据模板参数标准:

非类型模板参数必须具有结构类型,这是以下类型之一(可选择cv限定符,忽略限定符)

  • 左值引用类型(对对象或函数);
  • 整数类型;
  • 指针类型(对对象或函数);
  • 成员指针类型(对成员对象或成员函数);
  • 枚举类型;
  • std::nullptr_t; (自C++11起)

对于我们的情况,问题要求整数类型
那么,标准对整数类型有什么说法呢?
让我们看看std::is_integral以找出答案:

..., 如果 T 是类型 bool, char, char8_t (自 C++20 起), char16_t, char32_t, wchar_t, short, int, long, long long, 或任何实现定义的扩展整数类型,包括任何已签名的未签名的带有cv限定符的变体。

哎呀!

由于有9种类型 - 如果排除char8_t (仅适用于C++20)并认为实现定义的整数类型大多数情况下是这些整数类型的别名 - 一个人将不得不对以下内容进行专门化:

  • 9 signed
  • 9 signed const
  • 9 signed volatile
  • 9 signed const volatile
  • 这总共有36个特化版本。
  • 然后,再为无符号版本添加36个?!

免责声明

  • 毫无疑问,这就是为什么没有人(也许真的没有其他人)在此之前做过的原因...

等一下,等一下...

应该再考虑一下,并再次提出正确的问题:

  • 非类型参数如何“读取”/“解释”
  • 它是否有任何意义是volatile
  • 如果它的值是typename的一部分,那么const是否会在某种程度上被隐含

你肯定自己找到了答案...

同样地,char16_tchar32_twchar_t没有无符号版本。
此外,如果仔细阅读标准中关于模板参数的内容,可能会发现一些值得关注的东西...

非类型模板参数必须具有结构类型,这些类型是以下类型之一(可选择cv限定符,忽略修饰符

嗯,嗯,嗯...

- 这将比一个人最初期望的做更多的工作...:P
- 最终只需要14个Extract模板的特化就足以管理99%的所有可能的整数类型...

...我认为这对于如此少量的代码来说太多了。

请在下面找到解决方案,- 留给后代 - 希望它对某人有用(至少对第二个示例中使用的有趣的“诡计”有用)

° 个人评论

我很难相信这个九年前的问题没有早些时候得到答案 (也认为我不会是唯一一个找到答案的“蠢人”)


工作解决方案

解决方案 #1

这里没有什么特别的。它只是一个模板的常规专业化...

template<typename T, T V>
struct NonTypeParam { using Type = T; static constexpr T Value = V; };

namespace Details1 {

template<typename T> struct Extract { using Result = void; };

template<typename T, T V> using R = NonTypeParam<T, V>;

// boolean
template<bool V, template<bool> class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
// signed types
template<char      V, template<char>      class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<char16_t  V, template<char16_t>  class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<char32_t  V, template<char32_t>  class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<wchar_t   V, template<wchar_t>   class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<short     V, template<short>     class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<int       V, template<int>       class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<long      V, template<long>      class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<long long V, template<long long> class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
// unsigned types
template<unsigned char      V, template<unsigned char>      class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<unsigned short     V, template<unsigned short>     class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<unsigned int       V, template<unsigned int>       class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<unsigned long      V, template<unsigned long>      class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };
template<unsigned long long V, template<unsigned long long> class C> struct Extract<C<V>> { using Result = R<decltype(V), V>; };

} /* namespace Details1 */

template<typename T>
struct Extract1
{
    using Result = typename Details1::Extract<T>::Result;
};

// Victim template:
template<std::size_t I> struct TestNonType1 {};

// Usage:
using          Param  = typename Extract1<TestNonType1<42>>::Result;
using          PType  = typename Param::Type; // std::size_t
constexpr auto pValue = Param::Value;         // 42

解决方案 #2

在此解决方案中,使用decltype声明函数模板重载的能力,这些函数模板将永远不会被定义...

template<typename T, T V>
struct NonTypeParam { using Type = T; static constexpr T Value = V; };

namespace Details2 {

template<typename T, T V> using R = NonTypeParam<T, V>;

// boolean
template<bool V, template<bool> class C> R<decltype(V), V> Extract(C<V> && _);
// signed types
template<char      V, template<char>      class C> R<decltype(V), V> Extract(C<V> && _);
template<char16_t  V, template<char16_t>  class C> R<decltype(V), V> Extract(C<V> && _);
template<char32_t  V, template<char32_t>  class C> R<decltype(V), V> Extract(C<V> && _);
template<wchar_t   V, template<wchar_t>   class C> R<decltype(V), V> Extract(C<V> && _);
template<short     V, template<short>     class C> R<decltype(V), V> Extract(C<V> && _);
template<int       V, template<int>       class C> R<decltype(V), V> Extract(C<V> && _);
template<long      V, template<long>      class C> R<decltype(V), V> Extract(C<V> && _);
template<long long V, template<long long> class C> R<decltype(V), V> Extract(C<V> && _);
// unsigned types
template<unsigned char      V, template<unsigned char>      class C> R<decltype(V), V> Extract(C<V> && _);
template<unsigned short     V, template<unsigned short>     class C> R<decltype(V), V> Extract(C<V> && _);
template<unsigned int       V, template<unsigned int>       class C> R<decltype(V), V> Extract(C<V> && _);
template<unsigned long      V, template<unsigned long>      class C> R<decltype(V), V> Extract(C<V> && _);
template<unsigned long long V, template<unsigned long long> class C> R<decltype(V), V> Extract(C<V> && _);

} /* namespace Details2 */

template<typename T>
struct Extract2
{
    using Result = decltype(Details2::Extract(std::declval<T>()));
};

// Victim template:
template<unsigned long long I> struct TestNonType2 {};

// Usage:
using          Param  = typename Extract2<TestNonType2<42>>::Result;
using          PType  = typename Param::Type; // std::size_t
constexpr auto pValue = Param::Value;         // 42

° 更新(2021年7月25日)

  • 下面是一个使用任何类型的非类型参数声明的模板如何被分解的示例。
  • 不幸的是,尽管这段小代码似乎只使用了C++11语言特性,但它不能作为C++11编译。
  • 这段代码完美地工作,并且执行了它的任务,但必须编译为C++17。
  • 自添加auto作为非类型模板参数以来,标准肯定已经发生了变化,我认为(但找不到相关信息),使得编译器将模式<typename T,template <T> class C,T V>解释为<auto V>的同义词。
/* Template allowing to separately retrieve the components
 * of a template having one non-type parameter.
 */
template<typename T, template <T> class C, T V>
struct TmplInfo;

/* Function to decompose a template having one non-type
 * parameter and return its corresponding TmplInfo type.
 */
template<typename T, template <T> class C, T V>
inline constexpr TmplInfo<T, C, V> ToTmplInfo(C<V> && o);

/* Our victim template...
 */
template<std::size_t I> struct Victim;

/* Aliases Victim<42> and then decompose it to a TmplInfo.
 */
using V42   = Victim<42>;
using VInfo = decltype(ToTmplInfo(std::declval<V42>()));

/* Compiled for x64 arch, this gives:
 * using VInfo = TmplInfo<std::size_t, Victim, 42Ui64>;
 */

很高兴现在能够知道这是可能的!当我在2012年提出问题时,并不是所有这些功能都存在。 - glaebhoerl
1
@glaebhoerl 顺便提一下,需要注意的是,你在2012年使用的模式在C++17中被认可并且编译良好...我认为编译器会“引导”_typename TT x之间的关系,就好像x被声明为auto_一样... - Tenphase
1
感谢你对编辑所做出的所有努力!但是顺便说一句,原始问题并不仅仅关于整数类型 - 还有指针、成员指针等,它们在非类型模板参数中也是有效的。而且由于你可以有指向指针的指针等等,因此不幸的是不可能有限地枚举所有可能的类型,你确实需要某种通用解决方案。 - glaebhoerl
1
@glaebhoerl 我已经添加了一个更新说明(靠近顶部),并在最后添加了一个代码示例,显示实际上有一种方法来处理任何类型的非类型参数(已经是9年前的事情)... ;) - Tenphase
1
@glaebhoerl 我感到有点羞愧,我必须说。我进行了另一次编辑来纠正陈述...再试一次后,我已经能够进展一点,并为_integral pointer types_做了一些工作。我也有追踪如何使其适用于函数,但似乎没有办法使编译器推断指向类类型的指针的类型... ;( - 这意味着似乎没有机会使其适用于成员变量/函数的指针,这就结束了这段旅程,我想... - Tenphase
显示剩余5条评论

2
This other question is essentially asking the same thing, but for template type parameters instead of template non-type parameters. You can find it here: template metaprogramming: (trait for?) dissecting a specified template into types T<T2,T3 N,T4, ...> As for type parameters, it is actually quite simple. The code should look like this:
#include <tuple>
#include <vector>

template <class T> struct explode;

template <template <class... Args> class T, class... N>
struct explode<T<N...>>
{
    typedef T<N...> type;
    template <class... Args> using template_ = T<Args...>;
    template <int I> using type_parameter =
        typename std::tuple_element<I, std::tuple<N...>>::type;
};

#if TESTING
void test_harness()
{
    typedef explode<std::vector<int>> exv;

    exv::template_<char> vchar;  // The second parameter still has its default argument!
    exv::template_<exv::type_parameter<0>, exv::type_parameter<1>> vint;

    static_assert(std::is_same<exv::template_<char>, std::vector<char>>::value, "");
    static_assert(std::is_same<decltype(vchar), std::vector<char>>::value, "");
    static_assert(std::is_same<decltype(vint), std::vector<int>>::value, "");
    static_assert(std::is_same<exv::type, std::vector<int>>::value, "");
    static_assert(std::is_same<exv::type_parameter<0>, int>::value, "");
    static_assert(std::is_same<exv::type_parameter<1>, std::allocator<int>>::value, "");
}
#endif

但是对于非类型参数,我还没有确定是否可能。你可以从类似的代码开始

template <class... ArgTypes, template <ArgTypes... Args> class T, ArgTypes... N>
struct explode<T<N...>>
{
    typedef T<N...> type;
    template <ArgTypes... Args> using template_ = T<Args...>;
    template <int I> using type_of_parameter =
        typename std::tuple_element<I, std::tuple<ArgTypes...>>::type;
    template <int I> struct nontype_parameter {
        static constexpr type_of_parameter<I> value() {
            return std::get<I>(std::tuple<ArgTypes...>(N...));
        }
    };
};

};

但是Clang(至少)不接受它:

test.cc:8:8: warning: class template partial specialization contains a template
      parameter that can not be deduced; this partial specialization will never
      be used
struct explode<T<N...>>
       ^~~~~~~~~~~~~~~~
test.cc:7:20: note: non-deducible template parameter 'ArgTypes'
template <class... ArgTypes, template <ArgTypes... Args> class T, ArgTypes... N>
                   ^

即使你设法解决了那个问题,你仍然需要用手写的constexpr版本替换std::get,因为标准库的std::get不知道为什么不是constexpr


是的,这基本上是我在第一个示例中遇到的相同问题。如果模板参数仅出现在模板参数列表中而不出现在特化参数中,则无法推断它。据推测,标准规定了这一点,否则它看起来不像是不可能的事情。 - glaebhoerl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接