Гарантированно ли, что меня не укусит это нарушение ODR?

У меня есть заголовочный файл, который объявляет шаблон со статической переменной, а также определяет его:

/* my_header.hpp */
#ifndef MY_HEADER_HPP_
#define MY_HEADER_HPP_

#include <cstdio>

template<int n>
struct foo {
    static int bar;

    static void dump() { printf("%d\n", bar); }
};

template<int n>
int foo<n>::bar;

#endif // MY_HEADER_HPP_

Этот заголовок включен как main.cpp, так и общей библиотекой mylib. В частности, mylib_baz.hpp просто включает этот шаблон и объявляет функцию, которая изменяет специализацию шаблона.

/* mylib_baz.hpp */
#ifndef MYLIB_BAZ_HPP_
#define MYLIB_BAZ_HPP_

#include "my_header.hpp"

typedef foo<123> mylib_foo;
void init_mylib_foo();

#endif // MYLIB_BAZ_HPP_

а также

/* mylib_baz.cpp */
#include "mylib_baz.hpp"
void init_mylib_foo() {
    mylib_foo::bar = 123;
    mylib_foo::dump();
};

Когда я создаю mylib.so (содержащий mylib_baz.o), символ foo<123>::bar присутствует и объявляется слабым. Однако символ для foo<123>::bar объявлен слабым и в моем main.o:

/* main.cpp */
#include "my_header.hpp"
#include "mylib_baz.hpp"

int main() {
    foo<123>::bar = 456;
    foo<123>::dump(); /* outputs 456 */
    init_mylib_foo(); /* outputs 123 */
    foo<123>::dump(); /* outputs 123 -- is this guaranteed? */
}

Похоже, я нарушаю одно правило определения (foo<123>::bar определено как в my_header.cpp, так и в main.cpp). Однако и в g++, и в clang символы объявляются слабыми (или уникальными), так что меня это не укусит — все обращения к foo<123>::bar изменяют один и тот же объект.

Вопрос 1: Является ли это совпадением (может быть, ODR работает по-разному для статических членов шаблонов?) или мне действительно гарантируется такое поведение стандартом?

Вопрос 2. Как я мог предсказать наблюдаемое поведение? То есть что именно заставляет компилятор объявлять символ слабым?


person FreenodeForsakeMe    schedule 18.09.2015    source источник
comment
Я думаю, что стандарт говорит, что это не должно работать, но ваш компоновщик говорит, что это нормально. Нарушать ODR — плохая идея — код не везде будет работать. В C есть «общее расширение», позволяющее работать нескольким определениям (см. between-source-files-in-c/">Как мне использовать extern для совместного использования переменных между исходными файлами в C? — эта функция на самом деле является свойством компоновщика, и есть неплохая вероятность того, что ваши C и C++ компиляторы используют технологию компоновщика.)   -  person Jonathan Leffler    schedule 18.09.2015
comment
Согласовано! Я также почти уверен, что это не должно работать, но я не могу понять, почему. В моем случае я мог (и должен был!) объявить bar extern и определить его только в mylib_baz.cpp, решив эту проблему. Но мне действительно любопытно копнуть глубже и увидеть причину поведения, которое я наблюдаю.   -  person FreenodeForsakeMe    schedule 18.09.2015
comment
foo<123>::bar определены как в my_header.cpp, так и в main.cpp... Что? Я вижу, что это определено в одном месте, которое не является ни одним из упомянутых вами: my_header.hpp. Это одно определение.   -  person Barry    schedule 18.09.2015
comment
@Barry, заголовок включен в оба файла .cpp (через mylib_baz.hpp), которые затем компилируются отдельно. В итоге перед линковкой в ​​обоих .o файлах есть определение   -  person Maksim Solovjov    schedule 18.09.2015
comment
Вопрос в следующем: если вы меняете его в одной единице компиляции, всегда ли вы соблюдаете его в другом?   -  person Glenn Teitelbaum    schedule 18.09.2015


Ответы (1)


Нарушения ODR нет. У вас есть одно определение bar. Это здесь:

template<int n>
int foo<n>::bar; // <==

Поскольку bar равно static, это означает, что существует одно определение для всех единиц перевода. Несмотря на то, что bar появится один раз во всех ваших объектных файлах (в конце концов, им нужен символ для этого), компоновщик объединит их вместе, чтобы получить единственное истинное определение bar. Ты это видишь:

$ g++ -std=c++11 -c mylib_baz.cpp -o mylib_baz.o
$ g++ -std=c++11 -c main.cpp -o main.o
$ g++ main.o mylib_baz.o -o a.out

Производит:

$ nm mylib_baz.o | c++filt | grep bar
0000000000000000 u foo<123>::bar
$ nm main.o | c++filt | grep bar
0000000000000000 u foo<123>::bar
$ nm a.out | c++filt | grep bar
0000000000601038 u foo<123>::bar

Где u указывает:

"u"
Уникальный глобальный символ. Это расширение GNU для стандартного набора привязок символов ELF. Для такого символа динамический компоновщик позаботится о том, чтобы во всем процессе использовался только один символ с таким именем и типом.

person Barry    schedule 18.09.2015
comment
static точно не отвечает за это. Дело в том, что bar является членом шаблона и может быть создан (и унифицирован между единицами перевода) вместе с этим шаблоном. - person Davis Herring; 21.09.2017