Почему внутриклассовая инициализация статических членов нарушает ODR?

Есть несколько вопросов о переполнении стека в духе «почему я не могу инициализировать статические элементы данных в классе в C++». Большинство ответов цитируются из стандарта, говорящего вам, что вы можете сделать; те, кто пытается ответить почему, обычно указывают на ссылку (теперь кажущуюся недоступной) [EDIT: на самом деле она доступна, см. ниже] на сайте Страуструпа, где он заявляет, что разрешение инициализации статических членов в классе приведет к нарушать правило одного определения (ODR).

Однако эти ответы кажутся слишком упрощенными. Компилятор прекрасно справляется с проблемами ODR, когда захочет. Например, рассмотрим следующее в заголовке C++:

struct SimpleExample
{
    static const std::string str;
};

// This must appear in exactly one TU, not a header, or else violate the ODR
// const std::string SimpleExample::str = "String 1";

template <int I>
struct TemplateExample
{
    static const std::string str;
};

// But this is fine in a header
template <int I>
const std::string TemplateExample<I>::str = "String 2";

Если я создаю экземпляр TemplateExample<0> в нескольких единицах трансляции, срабатывает магия компилятора/компоновщика, и я получаю ровно одну копию TemplateExample<0>::str в конечном исполняемом файле.

Итак, мой вопрос: учитывая, что компилятор, очевидно, может решить проблему ODR для статических членов классов-шаблонов, почему он не может сделать это и для классов, не являющихся шаблонами?

EDIT: ответы на часто задаваемые вопросы Stroustrup доступны здесь. Соответствующее предложение:

Однако, чтобы избежать сложных правил компоновщика, C++ требует, чтобы каждый объект имел уникальное определение. Это правило было бы нарушено, если бы С++ разрешал определение сущностей внутри класса, которые необходимо хранить в памяти как объекты.

Однако кажется, что эти «сложные правила компоновщика» существуют и используются в случае шаблона, так почему бы не использовать его и в простом случае?


person Tristan Brindle    schedule 20.09.2013    source источник
comment
C++11 ослабляет это ограничение. Вы можете выполнять инициализацию в классе с помощью константных выражений.   -  person n. 1.8e9-where's-my-share m.    schedule 20.09.2013
comment
Да, но я понимаю, что даже в этом случае требуется, чтобы определение (без инициализатора) присутствовало в области пространства имен ровно в одной единице перевода; однако в случае шаблона он может появиться в заголовке и, следовательно, в нескольких TU. Мой вопрос заключается в том, почему магия объединения символов, используемая в случае шаблона, не может также использоваться для обычных классов, не являющихся шаблонами.   -  person Tristan Brindle    schedule 20.09.2013
comment
Шаблоны не были частью исходного языка C++.   -  person Raymond Chen    schedule 20.09.2013
comment
Почти дубликат.   -  person iammilind    schedule 20.09.2013
comment
C++17 допускает встроенную инициализацию членов статических данных (даже для нецелочисленных типов): inline static int x[] = {1, 2, 3};. См. en.cppreference.com/w/cpp/language/static#Static_data_members.   -  person Vladimir Reshetnikov    schedule 15.02.2018


Ответы (2)


Хорошо, следующий пример кода демонстрирует разницу между сильной и слабой ссылкой на компоновщик. После я попытаюсь объяснить, почему переход между ними может изменить результирующий исполняемый файл, созданный компоновщиком.

прототипы.h

class CLASS
{
public:
    static const int global;
};
template <class T>
class TEMPLATE
{
public:
    static const int global;
};

void part1();
void part2();

файл1.cpp

#include <iostream>
#include "template.h"
const int CLASS::global = 11;
template <class T>
const int TEMPLATE<T>::global = 21;
void part1()
{
    std::cout << TEMPLATE<int>::global << std::endl;
    std::cout << CLASS::global << std::endl;
}

файл2.cpp

#include <iostream>
#include "template.h"
const int CLASS::global = 21;
template <class T>
const int TEMPLATE<T>::global = 22;
void part2()
{
    std::cout << TEMPLATE<int>::global << std::endl;
    std::cout << CLASS::global << std::endl;
}

main.cpp

#include <stdio.h>
#include "template.h"
void main()
{
    part1();
    part2();
}

Я согласен, что этот пример полностью надуманный, но, надеюсь, он демонстрирует, почему «Изменение сильных ссылок на слабые компоновщики является критическим изменением».

Будет ли это компилироваться? Нет, потому что в нем есть 2 сильные ссылки на CLASS::global.

Если вы удалите одну из сильных ссылок на CLASS::global, будет ли она скомпилирована? Да

Каково значение TEMPLATE::global?

Какова ценность CLASS::global?

Слабая ссылка undefined, потому что она зависит от порядка ссылок, что делает ее в лучшем случае неясной и неконтролируемой в зависимости от компоновщика. Это, вероятно, приемлемо, потому что редко можно хранить весь шаблон в одном файле, потому что и прототип, и реализация необходимы вместе для того, чтобы компиляция работала.

Однако для членов класса статических данных, поскольку они исторически были сильными ссылками и не определялись в объявлении, было правилом, а теперь, по крайней мере, обычной практикой, иметь полное объявление данных со строгой ссылкой в ​​файле реализации.

На самом деле, из-за того, что компоновщик выдает ошибки связи ODR для нарушений сильных ссылок, было обычной практикой иметь несколько объектных файлов (связываемых единиц компиляции), которые были связаны условно, чтобы изменить поведение для различных аппаратных и программных комбинаций, а иногда и для преимущества оптимизации. Зная, что если вы допустили ошибку в параметрах ссылки, вы получите сообщение об ошибке либо о том, что забыли выбрать специализацию (нет строгой ссылки), либо выбрали несколько специализаций (несколько сильных ссылок).

Вы должны помнить, что во время появления C++ 8-битные, 16-битные и 32-битные процессоры все еще были действительными целями, у AMD и Intel были похожие, но разные наборы инструкций, поставщики оборудования предпочитали закрытые частные интерфейсы открытым стандартам. И цикл сборки может занять часы, дни и даже неделю.

person Strings    schedule 21.09.2013
comment
Спасибо за подробный ответ. Я предполагаю, что тогда это в основном сводится к истории - статические элементы данных были (и остаются) в основном просто глобальными переменными C с некоторыми ограничениями доступа во время компиляции, и сейчас слишком поздно что-то менять. - person Tristan Brindle; 01.10.2013

Раньше структура сборки C++ была довольно простой.

Компилятор создал объектные файлы, которые обычно содержали одну реализацию класса. Затем компоновщик объединил все объектные файлы в исполняемый файл.

Правило одного определения относится к требованию, чтобы каждая переменная (и функция), используемая в исполняемом файле, появлялась только в одном объектном файле, созданном компилятором. Все остальные объектные файлы просто имеют внешние ссылки на прототипы переменных/функций.

Шаблоны, добавленные в C++ очень поздно, и требуют, чтобы все детали реализации шаблона были доступны во время каждой компиляции каждого объекта, чтобы компилятор мог выполнить всю его оптимизацию - это требует большого количества встраивания и даже большего количества искажений имен.

Я надеюсь, что это отвечает на ваш вопрос, потому что это причина правила ODR и почему оно не влияет на шаблоны. Поскольку компоновщик почти не имеет ничего общего с шаблонами, все они управляются компилятором. Исключая случай, мы использовали специализацию шаблона, чтобы поместить все расширение шаблона в один объектный файл, чтобы его можно было использовать в других объектных файлах, если они видят только прототипы для шаблона.

Изменить:

В былые времена компоновщики часто связывали объектные файлы, созданные на разных языках. Было обычным делом связывать ASM и C, и даже после C++ часть этого кода все еще использовалась, и это абсолютно необходимо для ODR. Тот факт, что ваш проект связывает только файлы C++, не означает, что это все, что может сделать компоновщик, и поэтому он не будет изменен, поскольку большинство проектов теперь исключительно C++. Даже сейчас многие драйверы устройств используют компоновщик в соответствии с его более оригинальным намерением.

Ответ:

Однако кажется, что эти «сложные правила компоновщика» существуют и используются в случае шаблона, так почему бы не использовать его и в простом случае?

Компилятор управляет случаями шаблона и просто создает слабые ссылки компоновщика.

Компоновщик не имеет ничего общего с шаблонами, это шаблоны, используемые компилятором для создания кода, который он передает компоновщику.

Таким образом, шаблоны не влияют на правила компоновщика, но правила компоновщика по-прежнему важны, потому что ODR является требованием ASM и C, которые компоновщик все еще связывает, и люди, кроме вас, все еще фактически используют.

person Strings    schedule 20.09.2013
comment
компоновщик почти не имеет отношения к шаблонам ― это не совсем так. Компоновщик объединяет повторяющиеся функции и данные, если компилятор помечает их как объединяемые. Экземпляры шаблонов (функции и статические данные) так помечены, почему статические члены, не являющиеся шаблонами, не помечены? - person n. 1.8e9-where's-my-share m.; 20.09.2013
comment
@н.м. Точно, хорошо сказано - person Tristan Brindle; 20.09.2013
comment
Какой именно старый код будет взломан и каким образом? Не вижу причин для поломки. - person n. 1.8e9-where's-my-share m.; 20.09.2013
comment
Я не уверен, в чем заключается ваша точка зрения. Никто не собирается менять модель компиляции C++ — не нужно менять модель, какой бы она ни была. Текущие компоновщики вполне способны делать то, что мы хотим. Нам нужно только позволить компилятору генерировать правильные символы, что он уже делает в случае с шаблонами. Если вы не знаете, как злоупотреблять ODR — Злоупотребление — это мое второе имя, я не спрашиваю вас, как это сделать. Я спрашиваю, какой код будет взломан в соответствии с предложенным правилом и как. Вы, кажется, по какой-то причине совершенно уверены, что старый код будет сломан, и я не вижу этой причины. - person n. 1.8e9-where's-my-share m.; 20.09.2013
comment
@strings: отношение не нужно, я просто спрашивал, почему это конкретное средство C ++ имеет те ограничения, которые оно имеет, учитывая, что оно (по крайней мере, для не члена комитета) не кажется необходимым. Я не принял ваш ответ по причинам @n.m. дал: а именно, я искал конкретную причину, по которой символы-члены статических данных, не являющиеся шаблонами, не требуют по стандарту быть помеченными как weak, как шаблонные статические элементы данных. Сломать старый код кажется маловероятным, учитывая, что у меня действительно нет возможности сослаться, например, на SimpleExample::str из Фортрана. - person Tristan Brindle; 20.09.2013
comment
Изменение сильных ссылок компоновщика на слабые является критическим изменением — возможно, мы доживем до того, чтобы увидеть пример действительного кода, который будет сломан при таком изменении. - person n. 1.8e9-where's-my-share m.; 20.09.2013
comment
Есть даже вариант использования, как я описал — там нет кода C++. Я не знаю, насколько код, отличный от C++, имеет отношение к обсуждению. Где сильный ODR определяется в нескольких объектных файлах, которые выбираются во время компоновки. --- Извините, это предложение не анализируется. Вам нужно, чтобы я отредактировал все свои комментарии в своем ответе --- я не вижу, что они добавляют к вашему ответу. - person n. 1.8e9-where's-my-share m.; 21.09.2013
comment
Вы можете поиграть с определением этого класса и привести пример неработающей программы. Я пытался, но потерпел неудачу. - person n. 1.8e9-where's-my-share m.; 21.09.2013
comment
Ваше предположение неверно. Вы включаете один и тот же заголовочный файл в несколько исходных файлов, для этого и нужны заголовочные файлы. Свяжите получившиеся объекты вместе и посмотрите, что получится. Если вы хотите продемонстрировать неработающую валидную программу, вам нужно определить DO_NOT_EMIT_WEAK_SYMBOLS ровно для одного исходного файла и предоставить стандартное определение пространства имен Example::a в этом исходном файле. Не стесняйтесь изменить тип a на не-POD или что-то еще. - person n. 1.8e9-where's-my-share m.; 21.09.2013
comment
Я только что подумал об этом, когда отходил от клавиатуры. Давайте попробуем логическую логику... Страутроп говорит: «[Класс обычно объявляется в заголовочном файле] И [заголовочный файл обычно включается во многие единицы перевода]. ОДНАКО...'. В логическом значении A='класс, объявленный в заголовке', B='заголовок, включенный во многие ЕП', ОДНАКО=НЕ. Итак, !(A && B) == !A || !Б. Это означает, что найти причину, по которой его нельзя изменить (первоначально заданный вопрос), являются примеры, где «[класс не объявлен в заголовочном файле] ИЛИ [заголовочный файл не включен во многие единицы перевода]». - person Strings; 21.09.2013
comment
Жаль, что ты потерял меня здесь. Я не думаю, что продолжение этой дискуссии будет продуктивным. - person n. 1.8e9-where's-my-share m.; 22.09.2013