【问题标题】:Abstracting over types of non-type template parameters对非类型模板参数的类型进行抽象
【发布时间】:2012-05-25 18:57:36
【问题描述】:

我想编写一个模板,它可以将类型解构为具有非类型模板参数及其非类型模板参数的模板。例如,它将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&lt;T&gt; 我认为你也不能写 template&lt;T*&gt;。)

请不要问我为什么要问。

【问题讨论】:

  • 我必须说:我不明白你到底需要什么
  • 我相信可以总结如下:让template&lt;int&gt; struct A {}。是否可以(如果可以,如何)编写模板arg 使得arg&lt;Array&lt;5&gt; &gt;::template_template&lt;int&gt; Arrayarg&lt;Array&lt;5&gt; &gt;::typeintarg&lt;Array&lt;5&gt; &gt;::value5(类型为int ),并使其非常通用,以至于每个可能的非类型模板参数都可以用这种方式处理。
  • 基本上就是这样。感谢您说得比我更清楚!

标签: c++ templates


【解决方案1】:

答案可能来得有点晚......

错过的尝试...

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


保留这个原始答案作为我在 SO 上的第一个答案的纪念品。
不完全是失败,有人会说。而是第一次错过的尝试... ;)
现在,跳到下一条水平线...

当我遇到这个问题时,我正在寻找相关问题的答案。读完之后,我告诉自己:“嗯……那是我已经做过的事情。而且很有效。我是怎么做到的?!”。然后,我继续寻找我的问题的答案......

今天我觉得我应该花一点时间来提出这个问题的解决方案(实际上是两个)

正如您已经注意到的,问题出在编译器不知道如何推导出 T 的事实。可以将错误消息解释为“请给我一点帮助T

我所做的第一个工作版本具有foo 的专业化,派生自类似于std::integral_constant 的类。使foo 派生自std::integral_constant&lt;T, x&gt; 可能有助于编译器确定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 功能,则应该首选上面发布的解决方案,因为它管理的不仅仅是整数类型.

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

°序言

  • 经过我的研究,似乎解决方案这个特定问题还没有找到 (或未发表) 直到现在。我想我应该详细介绍一下“Whys”“Hows”我希望它会受到赞赏,但总的来说:有用...
  • 我目前正在编写一个包含编译时工具和功能的元编程库。我希望能够很快在 GitHub 上发布它。
    (谁知道)——无论如何...
  • 当我意识到我的第一个答案仅在 C++17 之后才正确时,我并没有感到沮丧...... - 不完全是“准时”,可以说。 ..:P
  • 考虑到 ATM 的所有“仅编译时”功能机制,我觉得 9 年前应该有办法做到这一点。
  • 我开始思考如果只使用 C++11 功能我会如何做到这一点,大约一个小时后,我找到了一个可行的解决方案(实际上是两个)
  • 我花了一点时间才使它成为一个可用的解决方案(实际上是两个)
  • 还有更多内容要纠正这篇文章... :D

    毕竟,可能有编译器
    “刚刚好” 可以理解仅 C++11...:P

显然,由于当时可用的功能集更窄,
找到的解决方案只是有点更冗长... :D

°搜索过程

首先,必须记住,当编译器输出 "cannot deduc"...
– 这并不意味着有错误(尽管可能有)
– 这意味着编译器并不像人们想象的那么聪明。
– 这意味着一个人必须帮助编译器才能完成它的工作......

清楚吗?
– 编译器恳请您完成其工作的一部分。
– 你有很好的机会:

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

这里,编译器说“无法推断T的类型”。
事实上,T 并没有用在作为 foo 特化的参数的表达式中,因此,它不能从那里推导出来......

首先必须做一些事情来表示typename Tx 的值(类型为T)之间的关系。立即想到的是,需要一个类似于 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,而是将type 赋予参数V...
现在,应该可以提出正确的问题了:

  1. 如何从等式中删除T
    • 通过显式定义V的类型。
  2. V 的值有哪些可能的类型?
    • 允许作为非类型模板参数的类型。

首先,例如,通过为char 显式定义V 的类型,将如何看待专业化?它看起来像这样:

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

不出意外,它按预期工作...
... 现在有哪些可能的类型?
按照template parameters上的标准:

一个非类型模板参数必须有一个结构类型,它是以下类型之一(可选cv-qualified,限定符被忽略)

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

对于我们的案例,问题要求整数类型
好吧,该标准对整数类型有何规定。
让我们看看std::is_integral就知道了:

...,如果 Tbool 类型,charchar8_t (C++20 起)char16_tchar32_t,@987654366 @、shortintlonglong long,或任何实现定义扩展整数类型,包括任何有符号unsignedcv-qualified 变体。

哎哟!

因为有 9 种类型 - 如果一个排除 char8_t (仅来自 C++20) 并认为实现定义的整数类型大多数时候是这些整数的别名类型 - 必须对以下方面进行专业化:

  • 9 signed.
  • 9signed const.
  • 9signed volatile.
  • 9signed const volatile.
  • 其中有 36 个专业。
  • 然后,为未签名的版本再添加 36 个?!

免责声明 [注意事项]

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

等一下,等一下……

人们应该再次考虑这一点,并再次提出正确的问题:

  • 非类型参数如何'read'/'interpreted'
  • volatile 是否有意义?
  • 如果它的值是 typename 的一部分,那么 const 是否以某种方式隐含

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

– 同样,没有unsigned 版本的char16_tchar32_twchar_t
– 此外,如果您再仔细阅读该标准中关于 template parameters 的内容,您可能会看到一些没有得到应有关注的内容...

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

好吧,好吧,好吧……

– 这将比一开始排除更多的工作...... :P
– 最终,Extract 模板的只有 14 个特化足以管理 99% 的所有可能的整数类型...

...对于这么少的代码,我认为写的太多了。

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

°个人评论

我很难相信这个 9 年前的问题还没有早点找到答案(以及认为我会是唯一找到这个答案的“愚蠢”人)


工作解决方案

解决方案#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 作为非类型模板参数以来,标准肯定发生了变化,我认为 (但找不到有关它的信息),使编译器将模式&lt;typename T, template &lt;T&gt; class C, T V&gt; 解释为好像它是&lt;auto V&gt;同义词
/* 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 作为旁注,需要注意的是,您在 2012 年使用的模式在 C++17 中被识别并编译得很好......我认为编译器“诱导” typename TT x 之间的关系,就好像声明了 x i>auto...
  • 感谢您为编辑付出的所有努力!但是FWIW,最初的问题只是关于整数类型——还有指针、成员指针等,它们作为非类型模板参数是有效的。而且由于您可以拥有指向指针等的指针,不幸的是不可能有限地枚举所有可能的类型,您确实需要某种通用解决方案。
  • @glaebhoerl 我已经添加了一个更新说明(靠近顶部),最后还有一个代码示例表明,事实上,有一种方法可以处理任何类型的非类型参数(已经9年前)... ;)
  • @glaebhoerl 我不得不说,我感到有点惭愧。我进行了另一次编辑以纠正该声明......在再次尝试之后(再次)我已经能够前进一点并为积分指针类型做了一些事情 那行得通。我也有关于如何使它适用于函数的轨道,但似乎没有办法让编译器推断出指向类类型的指针的类型......;( - 这意味着似乎没有机会使它适用于指向成员变量/函数的指针,我认为这将结束这段旅程......
【解决方案2】:

另一个问题基本上是问同样的事情,但对于模板 type 参数,而不是模板 non-type 参数:template metaprogramming: (trait for?) dissecting a specified template into types T<T2,T3 N,T4, ...>

对于type参数,真的很简单。代码如下所示:

#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 .

【讨论】:

  • 是的,这与我在第一个示例中遇到的问题基本相同。如果模板形参仅存在于模板形参列表中,但不存在于特化参数中,则无法推断。大概标准规定了这一点,因为否则它看起来不像是不可能的事情。
猜你喜欢
  • 2019-04-24
  • 2021-03-18
  • 1970-01-01
  • 1970-01-01
  • 2011-08-06
  • 1970-01-01
相关资源
最近更新 更多