【问题标题】:Undefined reference to static const int对静态 const int 的未定义引用
【发布时间】:2011-07-20 12:04:30
【问题描述】:

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

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 &amp; 参数,它似乎没有进行替换,而是更喜欢引用。我可以通过进行以下更改来解决此问题:

foo(static_cast<int>(kConst));

我相信这现在迫使编译器生成一个临时 int,然后传递一个对它的引用,它可以在编译时成功地做到这一点。

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

【问题讨论】:

  • 在实践中,您只需执行const int kConst = 1; 即可获得相同的结果。此外,很少有理由(我想不出)让函数采用 const int &amp; 类型的参数 - 只需在此处使用 int
  • @Space 真正的功能是一个模板,我会编辑我的问题来提一下。
  • @Space 仅供参考,不让它static 给出错误`ISO C++ 禁止初始化成员'kConst'...使'kConst' 成为静态。'
  • 我的错,谢谢指正。
  • 烦人的是,这个错误可能会出现在像std::min( some_val, kConst)这样的无害用法中,因为std::min&lt;T&gt;T const &amp;类型的参数,这意味着我们需要传递一个引用到 kConst。我发现它仅在优化关闭时发生。使用静态转换修复。

标签: c++ gcc


【解决方案1】:

这是故意的,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 草案中的规则:“名称显示为潜在求值表达式的变量或非重载函数是 odr-used,除非它是满足出现要求的对象在常量表达式 (5.19) 中,立即应用左值到右值的转换 (4.1)。”。静态转换立即执行左值-右值转换,因此在 C++0x 中它没有被“使用”。]

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

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

【讨论】:

  • 感谢您提供参考地址的示例。我认为这是编译器不符合我期望的真正实际原因。
  • 为了完全符合,我认为你必须定义类似template &lt;int N&gt; int intvalue() { return N; }的东西。然后使用intvalue&lt;kConst&gt;kConst 仅出现在需要整数常量表达式的上下文中,因此不使用。但是该函数返回一个与kConst 具有相同值的临时值,并且可以绑定到一个常量引用。不过,我不确定是否有更简单的方法可以便携地强制不使用kConst
  • 我在 gcc 4.7 中使用三元运算符(例如r = s ? kConst1 : kConst2)中的这种静态常量变量时​​遇到了同样的问题。我通过使用实际的if 解决了它。无论如何感谢您的回答!
  • ...和 ​​std::min / std::max,它把我带到了这里!
  • “AFAIK GCC 无法安排,除非对象定义在一个(且只有一个)TU 中”。这太糟糕了,因为已经可以使用常量来做到这一点——在 .rodata 中将它们多次编译为“弱 defs”,然后让链接器只选择一个——这样可以确保它的所有实际引用都具有相同的地址。这实际上是对 typeid 所做的;但是,当使用共享库时,它可能会以奇怪的方式失败。
【解决方案2】:

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

class Bar
{
      enum { kConst = 1 };
}

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

正确的代码应该是

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

【讨论】:

  • 感谢您提供这个说明性示例。
【解决方案3】:

这是一个非常有效的案例。尤其是因为 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”?
  • 你有什么例子吗?在提供的示例中,&gt;nm -C main.o | grep kConst 只给了我一行0000000000400644 R Bar::kConst
  • 编译静态库的时候看到的。
  • 在这种情况下,当然!静态库只是目标文件的集合。链接仅由静态库的客户端完成。因此,如果您放入存档文件、包含 const 定义的目标文件和另一个调用 Bar::func() 的目标文件,您将看到符号一次有定义,一次没有:nm -C lib.a 给你@987654327 @ 和 main_file.o: U Bar::kConst ....
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    我认为 C++ 的这种人工制品意味着任何时候引用 Bar::kConst 时,都会使用它的字面值。

    这意味着实际上没有变量可以引用。

    您可能必须这样做:

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

    【讨论】:

    • 这基本上是我通过将其更改为foo(static_cast&lt;int&gt;(kConst)); 来实现的,对吧?
    【解决方案6】:

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

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

    【讨论】:

    • 请注意,这需要声明中的 4 行和“常量”之后的大括号,因此您最终会编写 foo = std::numeric_limits::max() * bar::this_is_a_constant_that_looks_like_a_method () 并希望您的编码标准能够应对,并且优化器会为您修复它。
    【解决方案7】:

    简单的技巧:在传递函数的kConst 之前使用+。这将防止常量被引用,这样代码就不会生成对常量对象的链接器请求,而是继续使用编译器时的常量值。

    【讨论】:

    • 遗憾的是,当从声明时初始化的static const 值中获取地址时,编译器不会发出警告。这总是会导致链接器错误,并且当相同的常量也在目标文件中单独声明时,这也将是一个错误。编译器也完全了解情况。
    • 拒绝引用的最佳方法是什么?我目前正在做static_cast&lt;decltype(kConst)&gt;(kConst)
    • @Velkan 我也想知道该怎么做。在 kConst 是 char[64]; 的情况下,您的 tatic_cast(kConst) 技巧不起作用;它得到“错误:static_cast from 'char *' to 'decltype(start_time)' (aka 'char [64]') is not allowed”。
    • @DonHatch,我不喜欢软件考古,但据我所知,很难通过复制将原始数组传递给函数。因此,从语法上讲,原始问题中的 foo() 将需要该地址,并且没有将其视为整个数组的临时副本的机制。
    【解决方案8】:

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

    static constexpr int kConst = 1;
    

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

    【讨论】:

    • 引用它仍然需要它存在于链接器表中。至少,这就是我所看到的。
    • @Osman-pasha 是的,但是在 C++17 中,类中的这一行定义了一个内联变量,它自动管理定义。
    【解决方案9】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-17
      相关资源
      最近更新 更多