为什么结构体不能作为模板非类型参数以值传递?

17

非类型模板参数显然不是类型,例如:

template<int x>
void foo() { cout << x; }

在这种情况下,除了使用int之外,还有其他选项,我想引用这个很棒的答案
现在,有一件事让我感到困扰:结构体。考虑以下内容:
struct Triple { int x, y, z; };

Triple t { 1, 2, 3 };

template<Triple const& t>
class Foo { };

现在,使用正常的非类型引用语义,我们可以编写如下代码:
Foo<t> f;

值得注意的是,t 不能是 constexpr 或者 const,因为这意味着它具有内部链接,这基本上意味着该行代码将无法编译。我们可以通过将 t 声明为 const extern 来绕过这个问题。这本身可能有点奇怪,但真正让我想知道的是为什么不可能做到这一点:

Foo<Triple { 1, 2, 3 }> f;

我们从编译器得到了一个相当不错的错误信息:

错误:因为它不是左值,所以Triple{1,2,3}不是const Triple&类型的有效模板参数。

我们不能按值指定Triple模板,因为这是不允许的。然而,我无法理解这个小代码行背后的真实问题。为什么不允许使用结构体作为值参数?如果我可以使用三个int,那么为什么不能使用三个int的结构体?如果它只有平凡的特殊成员,那么与仅使用三个变量相比,它应该没有太大的区别。


2
标准只允许 int 和成员指针。你甚至不能将 double 或 float 作为非类型参数。即使是 const char*,它们都很有用 (并且在 VC6 上令人惊讶的工作正常)。根据某些答案,我不确定在 C++11 中是否有任何变化。 - Pete
1
我猜可能有太多的可能性无法为标准做出任何精细的调整。如果仅限于int,那么指定它会容易得多。也许在未来50年内,它将在下一个C++标准中提供。 - Pete
1
@Pete 你是通过使用 a == b 来测试两个浮点变量是否相等吗? - Bartek Banachewicz
1
同样的原因,没有隐式定义的operator==吗? - curiousguy
1
1)你应该使用像“string”和STL容器这样的工具来处理内存。2)对于任何包含指针的东西,如果成员逐个复制是不正确的(即如果它不是迭代器),那么成员逐个比较也是错误的! - curiousguy
显示剩余9条评论
3个回答

24

针对用户的更新答案:

C++20增加了对类字面量(具有constexpr构造函数的类)非类型模板参数的支持,这将允许原始问题中的示例正常工作,前提是模板参数被按值接受:

template<Triple t> // Note: accepts t by value
class Foo { };

// Works with unnamed instantiation of Triple.
Foo<Triple { 1, 2, 3 }> f1 {};

// Also works if provided from a constexpr variable.
constexpr Triple t { 1, 2, 3 };
Foo<t> f2 {};

进一步地,程序中所有的 Triple { 1, 2, 3 } 模板参数实例都将引用同一个静态存储期对象:same
template<Triple t1, Triple t2>
void Func() {
    assert(&t1 == &t2); // Passes.
}

constexpr Triple t { 1, 2, 3 };

int main()
{
    Func<t, Triple {1, 2, 3}>();
}

来自cppreference:

命名为非类型模板参数的标识符,其类别为T,表示一个静态存储期对象,类型为const T,称为模板参数对象。该对象的值是相应模板参数转换为模板参数类型后的值。程序中所有具有相同类型和值的这样的模板参数都表示相同的模板参数对象。

请注意,对于模板参数允许的类字面类型有相当多的限制。有关更多详细信息,请查看我撰写的博客文章,解释了C++20中字面类NTTP的用法和限制:Literal Classes as Non-type Template Parameters in C++20


2
哦,这太棒了!感谢您以如此高质量的帖子重新访问这个问题。 - Bartek Banachewicz

14

处理这一部分很容易,但人们会抱怨结构模板参数无法在所有情况下像其他模板参数一样使用(考虑部分特化或如何处理operator==)。

我认为,只得到蛋糕的一小块太凌乱了,而只得到一个微小的片段并不足够令人满意,甚至更加令人沮丧。仅处理这一小部分不会给我比以下代码更强大的功能,以下代码还具有额外的优势,可以直接处理各种类型的内容(包括部分特化)。

template <int X, int Y, int Z>
struct meta_triple {
    // static value getters
    static constexpr auto x = X;
    static constexpr auto y = Y;
    static constexpr auto z = Z;
    // implicit conversion to Triple 
    constexpr operator Triple() const { return { X, Y, Z }; }
    // function call operator so one can force the conversion to Triple with
    // meta_triple<1,2,3>()()
    constexpr Triple operator()() const { return *this; }
};

4
meta_triple是一个很棒的创作。它非常好地解决了我遇到的问题。 - Bartek Banachewicz
你能否编写一个函数,将 Triple 转换为 meta_triple 吗?同时也需要实现相反的转换过程。 - Eric
@Eric 不,这是单行道。 - R. Martinho Fernandes

7

你可以将 t 定义为 const extern,从而赋予它外部链接性。这时构造有效:

struct Triple { int x, y, z; };

const extern Triple t { 1, 2, 3 };

template<Triple const& t>
class Foo { };

Foo<t> f;

实时例子

无法将临时对象传递给引用模板参数的原因是该参数是引用。如果模板参数是const int&,并且您尝试传递7,则会出现相同的错误。示例

编辑

三个int和一个包含三个int的结构之间的区别在于类型为int的所有文字实际上都是相同的值(所有出现的7都只是七),而每次对结构的构造函数调用概念上创建一个新的实例。请看这个假设的例子:

template <Triple t>
struct Foo {};

Foo<Triple {1, 2, 3}> f1;
Foo<Triple {1, 2, 3}> f2;

我认为将这两个构造函数调用“匹配”到同一模板实例中会引入额外的复杂性。

3
嗯,问题归结为语言对POD结构体没有一个简单的op==,因此即使两个结构体内部具有完全相同的数据,在模板上下文中它们仍然是不同的。 - Bartek Banachewicz

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