对静态 const int 的未定义引用

Undefined reference to static const int

提问人:JaredC 提问时间:3/22/2011 最后编辑:JaredC 更新时间:7/15/2021 访问量:46198

问:

我今天遇到了一个有趣的问题。请看这个简单的例子:

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 intconst int &

foo(static_cast<int>(kConst));

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

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

C++ GCC

评论

1赞 Björn Pollex 3/22/2011
在实践中,你可以做同样的结果。此外,很少有理由(我想不出)让函数采用该类型的参数 - 只需使用 here。const int kConst = 1;const int &int
1赞 JaredC 3/22/2011
@Space真正的功能是一个模板,我将编辑我的问题以提及这一点。
1赞 JaredC 3/22/2011
@Space仅供参考,不这样做会给出错误“ISO C++禁止初始化成员”kConst“......使“kConst”成为静态。static
1赞 greggo 11/21/2015
令人讨厌的是,这个错误可能会出现在无害的用法中,例如 ,因为 具有 类型的参数 ,这意味着我们需要传递对 kConst 的引用。我发现它只在优化关闭时发生。修复了使用静态转换的问题。std::min( some_val, kConst)std::min<T>T const &
0赞 BIOStheZerg 11/1/2016
如果您不想进行太多更改,则快速解决方法建议:foo(kConst+0);

答:

1赞 quamrana 3/22/2011 #1

我认为 C++ 的这个工件意味着任何时候引用它时,都会使用它的文字值。Bar::kConst

这意味着在实践中,没有变量可以作为参考点。

您可能需要这样做:

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

评论

1赞 JaredC 3/22/2011
这基本上就是我通过将其更改为 来实现的,对吧?foo(static_cast<int>(kConst));
68赞 Steve Jessop 3/22/2011 #2

这是故意的,9.4.2/4 说:

如果静态数据成员是 const integral 或 const 枚举类型, 它在类中的声明 定义可以指定一个 constant-initializer,它应该是 积分常数表达式 (5.19) 在 在这种情况下,该成员可以出现在 积分常量表达式。这 成员仍应在 命名空间范围(如果用于 程序

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

可能会计算表达式 除非它出现在积分 常量表达式是必需的(请参阅 5.19),是 sizeof 运算符 (5.3.3) 的操作数,或者是 TypeId 运算符和表达式 不指定 多态类类型 (5.2.8)。一 对象或非重载函数是 如果其名称出现在 潜在评估的表达式。

所以事实上,当你也按值传递它时,你“使用”它,或者在 .只是 GCC 在一种情况下让你摆脱了困境,但在另一种情况下却没有。static_cast

[编辑:gcc 正在应用 C++0x 草案中的规则:“名称显示为潜在计算表达式的变量或非重载函数是 odr 使用的,除非它是满足常量表达式 (5.19) 中出现的要求的对象,并且立即应用左值到右值的转换 (4.1)。静态转换会立即执行左值-右值转换,因此在 C++0x 中它不会“使用”。

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

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

评论

1赞 JaredC 3/22/2011
感谢您以参考地址为例。我认为这是编译器没有达到我预期的真正实际原因。
0赞 Steve Jessop 3/22/2011
为了完全一致,我认为您必须定义类似 .然后 with , 只出现在需要整数常量表达式的上下文中,因此不使用。但是该函数返回一个与 具有相同值的临时值,并且可以绑定到 const 引用。不过,我不确定是否可能有一种更简单的方法来移植执行,但未使用。template <int N> int intvalue() { return N; }intvalue<kConst>kConstkConstkConst
1赞 Clodéric 1/9/2014
我在 gcc 4.7 的三元运算符中使用这种静态常量变量(即类似 )时遇到了同样的问题。我通过使用实际的.无论如何,感谢您的回答!r = s ? kConst1 : kConst2if
2赞 sage 3/2/2015
...和 std::min / std::max,它把我带到了这里!
0赞 greggo 11/21/2015
“AFAIK GCC 无法安排它,除非对象在一个(且只有一个)TU 中定义”。这太糟糕了,因为已经可以使用常量来做到这一点 - 在 .rodata 中将它们多次编译为“弱定义”,然后让链接器只选择一个 - 这确保了所有实际引用都具有相同的地址。这实际上是对 typeid 所做的;但是,当使用共享库时,它可能会以奇怪的方式失败。
2赞 TonyK 3/22/2011 #3

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

32赞 pelya 3/22/2011 #4

如果你在类声明中使用初始值设定项编写静态 const 变量,这就像你编写了

class Bar
{
      enum { kConst = 1 };
}

GCC 将以同样的方式对待它,这意味着它没有地址。

正确的代码应该是

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

评论

0赞 shuhalo 8/13/2017
谢谢你提供这个说明性的例子。
14赞 Stac 1/8/2015 #5

这是一个非常有效的案例。特别是因为 foo 可以是 STL 中的函数,例如 std::count,它采用常量 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”表示它是在数据部分定义的。

评论

0赞 quant_dev 1/3/2016
一个常数在“nm -C”输出中出现两次,首先是“R”和地址,然后是“U”,是否可以?
0赞 Stac 1/4/2016
你有什么例子吗?在提供的示例中,只给了我一行。>nm -C main.o | grep kConst0000000000400644 R Bar::kConst
0赞 quant_dev 1/5/2016
我在编译静态库时看到它。
1赞 Stac 1/6/2016
在这种情况下,当然!静态库只是对象文件的聚合。链接仅由静态库的客户端完成。因此,如果您放入存档文件,包含 const 定义的目标文件和另一个调用 Bar::func() 的目标文件,您将看到一次带有定义的符号,一次没有: 给你和 .nm -C lib.aConstants.o: 0000000000000000 R Bar::kConstmain_file.o: U Bar::kConst ...
1赞 Ethouris 9/19/2017 #6

简单诀窍:使用前传下来的功能。这将防止从中获取常量的引用,这样代码就不会生成对常量对象的链接器请求,而是会继续使用编译器时间常量值。+kConst

评论

1赞 Ethouris 9/21/2017
遗憾的是,当地址从声明中初始化的值中获取时,编译器不会发出警告。这始终会导致链接器错误,并且当在对象文件中单独声明相同的常量时,这也是一个错误。编译器也完全了解这种情况。static const
0赞 Velkan 6/21/2018
拒绝推荐信的最佳方法是什么?我目前正在做.static_cast<decltype(kConst)>(kConst)
0赞 Don Hatch 2/2/2019
@Velkan我也想知道该怎么做。你的 tatic_cast<decltype(kConst)>(kConst) 技巧在 kConst 是 char[64] 的情况下不起作用;它得到“错误:不允许从'char *'到'decltype(start_time)'(又名'char [64]')的static_cast”。
0赞 Velkan 2/4/2019
@DonHatch,我不喜欢软件考古学,但据我所知,通过复制将原始数组传递到函数中是非常困难的。因此,从语法上讲,来自原始问题的问题将需要地址,并且没有机制将其视为整个数组的临时副本。foo()
3赞 Yoav 11/19/2017 #7

您也可以将其替换为 constexpr 成员函数:

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

评论

0赞 Code Abominator 11/12/2018
请注意,这需要声明中的 4 行和“常量”后面的大括号,所以你最终会写出 foo = std::numeric_limits<int>::max() * bar::this_is_a_constant_that_looks_like_a_method() 并希望你的编码标准能够应对,优化器为你修复它。
0赞 Scg 8/24/2018 #8

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

7赞 Guillaume Racicot 7/15/2021 #9

在 C++17 中,正确的方法是简单地创建变量 constexpr:

static constexpr int kConst = 1;

constexpr 静态数据成员是隐式内联的。

评论

0赞 Osman-pasha 8/26/2021
引用它仍然需要它存在于链接器表中。至少,这就是我所看到的。
0赞 Guillaume Racicot 8/26/2021
@Osman-pasha是的,但是在C++17中,类中的这一行定义了一个内联变量,该变量自动管理定义。
0赞 iliar 12/29/2022
谢谢,您的回答是最有用的答案。