Неопределенная ссылка на static 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, чтобы справиться с этим случаем? Или мне почему-то не следует этого делать?

JaredC
источник
1
На практике вы можете добиться const int kConst = 1;того же результата. Кроме того, редко есть причина (я не могу придумать ни одной), чтобы функция принимала параметр типа const int &- просто используйте intздесь.
Björn Pollex
1
@Space, настоящая функция была шаблоном, я отредактирую свой вопрос, чтобы упомянуть об этом.
JaredC
1
@Space fyi, не создавая его, staticвыдает ошибку `ISO C ++ запрещает инициализацию члена 'kConst' ... делая 'kConst' статическим. '
JaredC
Мое плохое, спасибо за исправление.
Björn Pollex
1
Раздражает то, что эта ошибка может проявляться при безобидном использовании, например std::min( some_val, kConst), поскольку std::min<T>имеет параметры типа T const &, и подразумевается, что нам нужно передать ссылку на kConst. Я обнаружил, что это происходит только при отключенной оптимизации. Исправлено использование статического приведения.
greggo

Ответы:

61

Это намеренно, 9.4.2 / 4 говорит:

Если статический член данных имеет тип константного интегрального или константного перечисления, его объявление в определении класса может указывать константный инициализатор, который должен быть интегральным константным выражением (5.19). В этом случае член может появляться в интегральных константных выражениях. Член по-прежнему должен быть определен в области пространства имен, если он используется в программе.

Когда вы передаете статический член данных по константной ссылке, вы «используете» его, 3.2 / 2:

Выражение потенциально вычисляется, если оно не появляется там, где требуется интегральное постоянное выражение (см. 5.19), является операндом оператора sizeof (5.3.3) или операндом оператора typeid, а выражение не обозначает lvalue полиморфный тип класса (5.2.8). Объект или неперегруженная функция используется, если ее имя появляется в потенциально оцениваемом выражении.

Фактически, вы "используете" его, когда также передаете его по значению или в static_cast. Просто GCC позволил вам сняться с крючка в одном случае, но не в другом.

[Изменить: gcc применяет правила из черновиков C ++ 0x: «Переменная или неперегруженная функция, имя которой отображается как потенциально оцениваемое выражение, используется odr, если только это не объект, который удовлетворяет требованиям для появления в константе выражение (5.19) и преобразование lvalue-to-rvalue (4.1) применяется немедленно. ". Статическое приведение выполняет преобразование lvalue-rvalue немедленно, поэтому в C ++ 0x оно не «используется».]

Практическая проблема со ссылкой на const заключается в том, что fooон вправе взять адрес своего аргумента и сравнить его, например, с адресом аргумента из другого вызова, хранящегося в глобальном. Поскольку статический член данных является уникальным объектом, это означает, что если вы вызываете foo(kConst)из двух разных TU, то адрес переданного объекта должен быть одинаковым в каждом случае. AFAIK GCC не может этого добиться, если объект не определен в одном (и только одном) TU.

Хорошо, в данном случае fooэто шаблон, следовательно, определение видно во всех TU, поэтому, возможно, компилятор теоретически мог бы исключить риск того, что он что-то сделает с адресом. Но в целом вам, конечно, не следует брать адреса или ссылки на несуществующие объекты ;-)

Стив Джессоп
источник
1
Спасибо за пример взятия адреса ссылки. Я думаю, что это реальная практическая причина, почему компилятор не делает того, что я ожидаю.
JaredC
Для полного соответствия, я думаю, вам нужно определить что-то вроде template <int N> int intvalue() { return N; }. Тогда с intvalue<kConst>, kConstпоявляется только в контексте , требующий интегральное выражение постоянная, и поэтому не используются. Но функция возвращает временный объект с тем же значением kConst, что и, и который может связываться с константной ссылкой. Однако я не уверен, что может быть более простой способ перенести принудительное исполнение, kConstкоторое не используется.
Стив Джессоп,
1
У меня такая же проблема, когда я использую такую ​​статическую переменную const в тернарном операторе (то есть что-то вроде r = s ? kConst1 : kConst2) с gcc 4.7. Я решил это с помощью актуального файла if. В любом случае спасибо за ответ!
Clodéric
2
... и std :: min / std :: max, которые привели меня сюда!
sage
«AFAIK GCC не может этого добиться, если объект не определен в одном (и только одном) TU». Это очень плохо, так как это уже возможно сделать с константами - скомпилируйте их несколько раз как 'weak defs' в .rodata, а затем пусть компоновщик просто выберет один - что гарантирует, что все фактические ссылки на него будут иметь один и тот же адрес. Именно это и делается для typeid; однако при использовании общих библиотек он может терпеть неудачу по странным причинам.
greggo
27

Если вы пишете статическую переменную const с инициализатором внутри объявления класса, это как если бы вы написали

class Bar
{
      enum { kConst = 1 };
}

и GCC будет относиться к нему так же, что означает, что у него нет адреса.

Правильный код должен быть

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», означающую, что она определена в разделе данных.

Stac
источник
Это нормально, что константа появляется в выводе «nm -C» дважды, сначала с «R» и адресом, а затем с «U»?
Quant_dev 02
У вас есть пример? В приведенном примере >nm -C main.o | grep kConstдает мне только одну строку 0000000000400644 R Bar::kConst.
Stac
Я это вижу при компиляции статической библиотеки.
Quant_dev 05
1
В этом случае, конечно! Статическая библиотека - это всего лишь совокупность объектных файлов. Связывание выполняется только клиентом статической библиотеки. Итак, если вы поместите в архивный файл объектный файл, содержащий определение константы, и другой объектный файл, вызывающий Bar :: func (), вы увидите символ один раз с определением и один раз без: nm -C lib.aдает вам Constants.o: 0000000000000000 R Bar::kConstи main_file.o: U Bar::kConst ....
Stac
2

Я думаю, что этот артефакт C ++ означает, что всякий раз, когда на Bar::kConstнего ссылаются, вместо него используется его буквальное значение.

Это означает, что на практике нет переменной, на которую можно было бы ссылаться.

Возможно, вам придется сделать это:

void func()
{
  int k = kConst;
  foo(k);
}
Quamrana
источник
Это в основном то, чего я добился, изменив его foo(static_cast<int>(kConst));, верно?
JaredC
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 () и надеетесь, что ваши стандарты кодирования справятся и оптимизатор исправит это за вас.
Code Abominator
1

Простой трюк: используйте +перед kConstпереданной функцией. Это предотвратит взятие ссылки на константу, и таким образом код не будет генерировать запрос компоновщика к константному объекту, а вместо этого продолжит работу со значением константы времени компилятора.

Ethouris
источник
Жаль, что компилятор не выдает предупреждения, когда адрес берется из static constзначения, которое инициализируется при объявлении. Это всегда приводило к ошибке компоновщика, и когда та же самая константа также объявлялась отдельно в объектном файле, это тоже было бы ошибкой. Компилятор также полностью осведомлен о ситуации.
Ethouris
Как лучше всего отказаться от ссылок? Я сейчас делаю static_cast<decltype(kConst)>(kConst).
Velkan
@Velkan Я тоже хотел бы знать, как это сделать. Ваш трюк с tatic_cast <decltype (kConst)> (kConst) не работает в случае, если kConst является char [64]; он получает сообщение «error: static_cast from 'char *' to 'decltype (start_time)' (aka 'char [64]') is not allowed».
Дон Хэтч
@DonHatch, я не занимаюсь археологией программного обеспечения, но насколько я помню, очень сложно передать необработанный массив в функцию путем копирования. Таким образом, синтаксически foo()исходный вопрос будет нуждаться в адресе, и нет механизма, чтобы рассматривать его как временную копию всего массива.
Велкан 03
0

У меня возникла та же проблема, о которой упоминал Cloderic (static const в тернарном операторе r = s ? kConst1 : kConst2:), но он пожаловался только после отключения оптимизации компилятора (-O0 вместо -Os). Произошло на gcc-none-eabi 4.8.5.

SCG
источник