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

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

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

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 для статических членов классов шаблонов, почему это не может быть сделано для классов без шаблонов?

РЕДАКТИРОВАТЬ. Ответ на часто задаваемые вопросы в Stroustrup здесь. Соответствующее предложение:

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

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

Ответ 1

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

prototypes.h

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

void part1();
void part2();

file1.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;
}

file2.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 для нарушения сильных ссылок, было обычной практикой иметь несколько объектных файлов (блоки компиляции), которые были условно связаны с изменением поведения для различных аппаратных и программных комбинаций а иногда и для оптимизации. Зная, что вы допустили ошибку в своих параметрах ссылок, вы получите сообщение об ошибке, указав, что вы забыли выбрать специализацию (без сильной ссылки) или выбрали несколько специализаций (несколько сильных ссылок)

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

Ответ 2

Структура сборки С++ была довольно простой.

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

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

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

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

Edit:

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

Ответ:

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

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

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

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