未定义对静态const int的引用


79

我今天遇到了一个有趣的问题。考虑以下简单示例:

template <typename T>
void foo(const T & a) { /* code */ }

// This would also fail
// void foo(const int & a) { /* code */ }

class Bar
{
public:
   static const int kConst = 1;
   void func()
   {
      foo(kConst);           // This is the important line
   }
};

int main()
{
   Bar b;
   b.func();
}

编译时出现错误:

Undefined reference to 'Bar::kConst'

现在,我非常确定这是因为static const int没有在任何地方定义,这是有意的,因为根据我的理解,编译器应该能够在编译时进行替换,而无需定义。但是,由于函数带有const int &参数,因此似乎没有进行替换,而是选择了引用。我可以通过以下更改来解决此问题:

foo(static_cast<int>(kConst));

我认为这现在迫使编译器创建一个临时int,然后传递对该引用的引用,它可以在编译时成功完成。

我想知道这是否是故意的,还是我对gcc期望太多,无法处理此案?还是出于某种原因我不应该这样做?


1
实际上,您可以const int kConst = 1;得到相同的结果。另外,很少有理由(我想不到的)让函数采用类型的参数const int &-只需在int此处使用。
比约恩博动

1
@Space真正的功能是一个模板,我将编辑我的问题以提及这一点。
JaredC 2011年

1
@Space fyi,没有static给出错误,`ISO C ++禁止初始化成员'kConst'...使'kConst'成为静态。
JaredC 2011年

我不好,谢谢你的纠正。
比约恩博动

1
恼人的是,这个错误可能会出现在像无害的用途std::min( some_val, kConst),因为std::min<T>有一个类型的参数T const &,并且其含义是,我们需要传递给一个参考kConst。我发现只有在优化关闭时才会发生。使用静态强制转型。
greggo

Answers:


61

这是有意的,9.4.2 / 4说:

如果静态数据成员是const整数或const枚举类型,则其在类定义中的声明可以指定一个常量初始化器,该初始化器应为整数常量表达式(5.19)。在这种情况下,该成员可以出现在整数常量表达式中。如果程序中使用了该成员,则该成员仍应在命名空间范围内定义

当通过const引用传递静态数据成员时,可以“使用” 3.2 / 2:

表达式可能会被求值,除非它出现在需要整数常量表达式的位置(请参阅5.19),是sizeof运算符(5.3.3)的操作数或typeid运算符的操作数且该表达式未指定的左值多态类类型(5.2.8)。如果对象或非重载函数的名称出现在可能求值的表达式中,则使用该函数。

因此,实际上,当您也按值或在值中传递它时,便“使用”它static_cast。只是GCC在一种情况下让您摆脱了困境,而在另一种情况下却没有。

[编辑:gcc正在应用C ++ 0x草案中的规则:“除非其对象满足以常数形式出现的要求,否则将使用其名称显示为可能评估的表达式的变量或非重载函数。表达式(5.19)和左值到右值转换(4.1)立即应用。”。静态强制类型转换立即执行左值-右值转换,因此在C ++ 0x中不“使用”。]

const引用的实际问题在于,foo它有权获取其自变量的地址,并将其与例如存储在全局变量中的另一个调用的自变量的地址进行比较。由于静态数据成员是唯一的对象,因此这意味着,如果您foo(kConst)从两个不同的TU进行调用,则传递的对象的地址在每种情况下必须相同。除非在一个(只有一个)TU中定义了对象,否则AFAIK GCC不能安排该操作。

好的,因此在这种情况下foo是模板,因此该定义在所有TU中都可见,因此也许编译器理论上可以排除使用该地址执行任何操作的风险。但总的来说,您当然不应该使用不存在的对象的地址或引用;-)


1
感谢您提供引用地址的示例。我认为这是编译器未按预期执行的真正实际原因。
JaredC 2011年

为了完全一致,我认为您必须定义template <int N> int intvalue() { return N; }。然后with intvalue<kConst>kConst仅出现在需要整数常量表达式的上下文中,因此不使用。但是该函数返回的临时值与相同kConst,并且可以绑定到const引用。不过,我不确定,也许有一种更简单的方法可以移植执行,而这kConst是未使用的。
史蒂夫·杰索普

1
通过在r = s ? kConst1 : kConst2gcc 4.7的三元运算符(例如)中使用此类静态const变量,我遇到了相同的问题。我通过使用实际解决了if。无论如何,谢谢您的回答!
Clodéric

2
...和std :: min / std :: max,这导致我来到这里!
2015年

“除非在一个(并且只有一个)TU中定义了对象,否则AFAIK GCC不能安排它”。太糟糕了,因为已经可以使用常量来做到这一点-将它们作为.rodata中的“弱定义”多次编译,然后让链接器仅选择一个-这样可以确保对它的所有实际引用都具有相同的地址。这实际上是对typeid所做的;但是,当使用共享库时,它可能会以怪异的方式失败。
greggo,2015年

27

如果您要在类声明中使用初始化程序编写静态const变量,就好像您已经编写了

class Bar
{
      enum { kConst = 1 };
}

而海湾合作委员会将以同样的方式对待它,这意味着它没有地址。

正确的代码应该是

class Bar
{
      static const int kConst;
}
const int Bar::kConst = 1;

谢谢您的说明性示例。
shuhalo

12

这是一个非常有效的案例。特别是因为foo可能是STL中的函数,如std :: count,它将const T&作为其第三个参数。

我花了很多时间试图理解链接器为什么在这样的基本代码上有问题。

错误讯息

未定义对“ Bar :: kConst”的引用

告诉我们链接器找不到符号。

$nm -C main.o
0000000000000000 T main
0000000000000000 W void foo<int>(int const&)
0000000000000000 W Bar::func()
0000000000000000 U Bar::kConst

从“ U”中我们可以看到Bar :: kConst是未定义的。因此,当链接器尝试执行其工作时,它必须找到该符号。但是,您仅声明kConst而不定义它。

C ++中的解决方案还定义如下:

template <typename T>
void foo(const T & a) { /* code */ }

class Bar
{
public:
   static const int kConst = 1;
   void func()
   {
      foo(kConst);           // This is the important line
   }
};

const int Bar::kConst;       // Definition <--FIX

int main()
{
   Bar b;
   b.func();
}

然后,您可以看到编译器会将定义放入生成的目标文件中:

$nm -C main.o
0000000000000000 T main
0000000000000000 W void foo<int>(int const&)
0000000000000000 W Bar::func()
0000000000000000 R Bar::kConst

现在,您可以看到“ R”字样,它是在数据部分中定义的。


在“ nm -C”输出中两次出现一个常量,首先带有“ R”和地址,然后带有“ U”,是否可以?
quant_dev

你有什么例子吗?在提供的示例中,>nm -C main.o | grep kConst仅给我一行0000000000400644 R Bar::kConst
斯塔克

我在编译静态库时看到它。
–quant_dev

1
在这种情况下,当然可以!静态库只是目标文件的集合。链接仅由静态库的客户端完成。因此,如果您将包含const定义的目标文件和另一个名为Bar :: func()的目标文件放入归档文件中,则将看到一次带有该定义的符号,一次不带有该符号的符号:nm -C lib.a给您Constants.o: 0000000000000000 R Bar::kConstmain_file.o: U Bar::kConst ...
斯塔克

2

g ++版本4.3.4接受此代码(请参阅此链接)。但是g ++版本4.4.0拒绝了它。


2

我认为C ++的这种伪像意味着Bar::kConst在引用的任何时间都将使用其字面值。

这意味着在实践中没有可参考的变量。

您可能需要这样做:

void func()
{
  int k = kConst;
  foo(k);
}

这基本上就是我将其更改为所实现的foo(static_cast<int>(kConst));,对吗?
JaredC 2011年

2

您也可以用constexpr成员函数替换它:

class Bar
{
  static constexpr int kConst() { return 1; };
};

请注意,这需要在声明中同时包含4行,并在“常量”之后使用大括号,因此最终您需要编写foo = std :: numeric_limits <int> :: max()* bar :: this_is_a_constant_that_looks_like_a_method()并希望您的编码标准能够应对优化器会为您修复它。
代码憎恶者'18

1

简单窍门:+kConst传递函数之前使用。这将防止从该常量中获取该常量的引用,这样,代码将不会生成指向该常量对象的链接器请求,但它将继续使用编译时常量值。


遗憾的是,当从static const声明中初始化的值中获取地址时,编译器没有发出警告。这将始终导致链接器错误,并且当在对象文件中也分别声明相同的常量时,这也将是一个错误。编译器也完全了解这种情况。
Ethouris

拒绝引用的最佳方法是什么?我正在做static_cast<decltype(kConst)>(kConst)
Velkan

@Velkan我也想知道该怎么做。如果kConst是char [64],则您的tatic_cast <decltype(kConst)>(kConst)技巧无效。它会收到“错误:static_cast从'char *'到'decltype(start_time)'(aka'char [64]')不允许”。
唐·哈奇

@DonHatch,我不是软件考古学家,但是据我所知,通过复制将原始数组传递给函数非常困难。因此,foo()从语法上讲,原始问题中的地址将需要地址,并且没有机制将其视为整个数组的临时副本。
Velkan

0

我遇到了与Cloderic(三元运算符中的静态const :)相同的问题r = s ? kConst1 : kConst2,但仅在关闭编译器优化(-O0而不是-Os)后才抱怨。发生在gcc-none-eabi 4.8.5上。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.