使用模板参数大小的const字符数组 vs 字符指针

11

我今天在一些代码中看到了以下类型的结构:

template<unsigned int N> unsigned int f(const char (&a)[N]);

这个和下面这个相比,有什么合理的优点吗:

unsigned int f(const char *a);

我对后面的指针共享影响有点理解,但它真的如此糟糕,以至于需要用两倍大小的晦涩代码来替换吗?

(不幸的是,我无法询问代码的作者,否则我会这样做)


在第二个例子中,a 指向什么?一个单独的 int?一个简单的数组?一个 C 字符串?一个以 null 结尾的数组?明白我的意思了吗? - Shoe
6个回答

19

将任何裸指针传递给函数的目的是为了调用方能够对以下内容有所了解:

  • 它指向什么。
  • 它指向多少个。

作为输入参数的C风格字符串,后者是可以推断出来的,因为假设"多少个"是通过到达空字符终止符来确定的。

但是如果您没有传递C风格字符串呢?如果它只是一个零个或更多char值的序列呢?那么,如果是这种情况,那么:

void f(const char *s)
{
    // how many char does s refer to?
}

逻辑推断应该是这样做:

void f(const char *s, std::size_t N)
{
    // Now the caller is telling us there are N chars at s
}

这种情况并不罕见,但如果调用者传递了错误的长度(永远不要说永远),那么可能会出现错误。

但是,如果有一种方法可以通过非类型模板参数的推导从传递给函数的变量类型中吃掉该数据呢? 如果调用者正在使用固定数组来调用我们,会怎样呢?

template<std::size_t N>
void f(const char(&ar)[N])
{
    // we know the caller is passing a const-reference to a
    // char array declared of size N. The value N can be used
    // in this function.
}

现在我们知道列表中的两个项目: "什么" 和 "多少"。此外,我们现在可以提供模板函数重载,并且两者都可用于我们:

// length specified implementation
void f(const char *s, std::size_t N)
{
    // caller passed N
}

// fixed buffer template wrapper
template<std::size_t N>
void f(const char(&ar)[N])
{
    f(ar,N); // invokes length-specified implementation from above.
}

以下两者都能正常工作:

int main()
{
    char buff[3];

    f(buff,3);
    f(buff);

}

那么这有什么好处呢?因为以下代码将会发生编译错误,因为找不到匹配的实现:

int main()
{
    char buff[3];
    const char *ptr = buff;
    f(ptr); // ERROR: no matching function f(const char *)
}

总的来说,这是一种常见的技术,可以帮助我们向调用者提供列表中的两个项目:"是什么"和"多少",而不必每次在使用固定长度本地数组作为输入参数时都手写sizeof(ar)/sizeof(*ar)

祝你好运。


传递rvalue会导致奇怪的行为,因为“aaaa”在模板函数中的大小为7。为什么? - Sandburg
@Sandburg 我无法重现那个问题:在此处查看实时演示 - WhozCraig

4
指针并不会保留它们指向单个对象还是数组的第一个对象的信息。因此,如果您在函数体内编写以下内容:
unsigned int f(const char *a);

表达式

sizeof( a ) / sizeof( *a )

如果您在函数体内使用相同的表达式,您将仅获得指针本身的大小。

template<unsigned int N> unsigned int f(const char (&a)[N]);

你将得到数组的大小(当然,你也可以使用 N 的值)。

因此,当使用第二种方法时,通常会声明带有两个参数的函数,其中第二个参数指定数组的大小。

unsigned int f(const char *a, size_t n);

考虑这样一种情况,当你需要将作为参数传递的字符数组与另一个字符串附加在一起时(假设没有常量限定符),如果使用第二种声明,则即使将指针应用于函数strlen,也无法帮助您确定原始字符串是否足够大以进行附加。在第一种方法中,您可以使用表达式N - strlen(a)来确定数组中是否有足够的空间。

3
template<unsigned int N> unsigned int f(const char (&a)[N]);

这个函数模板通过引用接收数组,这意味着当直接与静态分配的C数组一起使用时,函数在编译时知道数组大小。

unsigned int f(const char *a);

这里所有的函数都知道的是它们被给了一个指向const char的指针。因此,如果您想将其用于操纵数组,则必须额外提供数组大小作为参数,因为无法仅从指针中检索此信息。


2

模板版本接受一定大小的数组。指针版本仅接受指针,没有关于指针所指数据大小的概念。


1
template会为每个不同的N值实例化(因此会生成额外的机器码),但在f内部,它可以自由地使用它对N的知识来执行任何有用的操作(例如,从a中处理正确数量的条目,相应地调整另一个可能需要的字符数组的大小)。要知道N是否实际被使用以及是否有用,必须查看f的内部。非模板版本中,调用者不提供数组/数据大小,因此没有提供等效信息(如果数据a指向的长度是可变的,则f必须依赖其他方式来知道有多少数据,例如,strlen()花费时间搜索终止NUL)。

你的 strlen 示例是错误的。无论如何,如果真实字符串大小小于数组大小>o<,我们应该搜索 '\0'。 - ikh
@ikh:这并不意味着我的例子是错误的——这只是一个被支持的用法问题,而模板形式通常会被精确地执行,因为在该用法中N值将有意义,无需使用strlen等函数(即使有时仅提供有用的上限)。 - Tony Delroy

1

使用它的两个原因是:

  • 只有c风格数组才能传递给该模板函数,因此使用指针调用该方法将导致编译错误。
  • 在模板函数中,您可以获取在编译期间计算出的数组大小。

因此,这个:

const char* p="abc";
f(p);

导致编译失败的原因是:
garbage.cpp:39:5: error: no matching function for call to ‘f(const char*&)’
  f(p);
     ^
garbage.cpp:39:5: note: candidate is:
garbage.cpp:21:39: note: template<unsigned int N> unsigned int f(const char (&)[N])
 template<unsigned int N> unsigned int f(const char (&a)[N])

而这个:

f("abc");

这是可以的,你可以将此数组的大小确定为编译常量


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