Невизначене посилання на статичний 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тут.
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 говорить:

Якщо статичний елемент даних має тип const інтеграл або тип const перелічення, його оголошення у визначенні класу може вказувати константу-ініціалізатор, яка повинна бути інтегральним константним виразом (5.19). У цьому випадку член може з'являтися у інтегральних константних виразах. Член все ще повинен бути визначений у області простору імен, якщо він використовується в програмі

Коли ви передаєте статичний елемент даних за посиланням const, ви "використовуєте" його, 3.2 / 2:

Вираз потенційно обчислюється, якщо він не з'являється там, де потрібен інтегральний константний вираз (див. 5.19), є операндом оператора sizeof (5.3.3) або операндом оператора типового оператора, і вираз не позначає значення l тип поліморфного класу (5.2.8). Об'єкт або незавантажена функція використовується, якщо його ім'я відображається у потенційно оціненому виразі.

Отже, насправді ви "використовуєте" його, коли передаєте його також за значенням, або в a static_cast. Просто GCC в одному випадку відпустив вас, але не в іншому.

[Редагувати: gcc застосовує правила з чернеток C ++ 0x: "Змінна або незавантажена функція, ім'я якої відображається як потенційно обчислюваний вираз, підтримується, якщо це не об'єкт, який задовольняє вимогам щодо появи у константі вираз (5.19) і негайно застосовується перетворення lvalue в 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, що і може прив'язуватися до посилання const. Я не впевнений, однак, може існувати простіший спосіб портативного забезпечення, який kConstне використовується.
Steve Jessop

1
Я відчуваю ту ж проблему, використовуючи таку статичну змінну const у тернарному операторі (тобто щось на зразок r = s ? kConst1 : kConst2) з gcc 4.7. Я вирішив це за допомогою фактичного if. У будь-якому разі дякую за відповідь!
Клодерік

2
... і std :: min / std :: max, що привело мене сюди!
мудрець

"AFAIK GCC не може це домовитись, якщо об'єкт не визначений в одному (і лише одному) TU". Це дуже погано, оскільки це вже можливо зробити з константами - компілювати їх кілька разів як `` слабкі деф '' у .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 і не визначаєте його.

Рішенням на С ++ також є його визначення наступним чином:

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 02

У вас є якийсь приклад? На наведеному прикладі подається >nm -C main.o | grep kConstлише один рядок 0000000000400644 R Bar::kConst.
Stac

Я бачу це при компіляції статичної бібліотеки.
quant_dev

1
У цьому випадку, звичайно! Статична бібліотека - це лише сукупність об’єктних файлів. Зв'язок здійснюється лише клієнтом статичної бібліотеки. Отже, якщо ви помістите в архівний файл, об’єктний файл, що містить визначення const та інший об’єктний файл, що викликає Bar :: func (), ви побачите символ один раз із визначенням і один раз без: nm -C lib.aдає вам Constants.o: 0000000000000000 R Bar::kConstі main_file.o: U Bar::kConst ....
Stac


2

Я думаю, що цей артефакт С ++ означає, що будь-який час, на який Bar::kConstйдеться, замість нього використовується його буквальне значення.

Це означає, що на практиці не існує змінної, на яку можна зробити посилання.

Можливо, вам доведеться зробити це:

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

Це в основному те, чого я домігся, змінивши це 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переданою функцією. Це не дозволить константі брати посилання, і таким чином код не генерує запит на зв'язування до об'єкта константи, але замість цього буде продовжувати значення константи часу компілятора.


Шкода, однак, що компілятор не видає попередження, коли адреса береться зі static constзначення, яке ініціалізується в декларації. Це завжди призводить до помилки компонувальника, і коли та сама константа також оголошується окремо в об'єктному файлі, це теж буде помилкою. Компілятор також повністю усвідомлює ситуацію.
Ethouris

Який найкращий спосіб відмовитись від посилань? Я зараз роблю static_cast<decltype(kConst)>(kConst).
Велкан

@Velkan Я б хотів знати, як це зробити теж. Ваш трюк tatic_cast <decltype (kConst)> (kConst) не працює, якщо kConst є символом [64]; він отримує "помилку: static_cast від 'char *' до 'decltype (start_time)' (він же" char [64] ') не допускається ".
Дон Хетч,

@DonHatch, я не займаюся програмною археологією, але, наскільки я пам’ятаю, дуже важко передати необроблений масив у функцію шляхом копіювання. Тож синтаксично для foo()вихідного запитання знадобиться адреса, і немає механізму, щоб розглядати це як тимчасову копію цілого масиву.
Велкан

0

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.