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


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

Однако эти ответы кажутся чрезмерно упрощенными. Компилятор прекрасно может разобраться в проблемах 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++ требует, чтобы каждый объект имел уникальное определение. Это правило было бы нарушено, если бы C++ разрешил определение в классе сущностей, которые должны храниться в памяти в виде объектов

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

2 12

2 ответа:

Хорошо, следующий пример кода демонстрирует разницу между сильной и слабой ссылками компоновщика. После я попытаюсь объяснить, почему изменение между 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;
}

Главная.cpp

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

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

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

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

Какова ценность шаблона:: global?

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

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

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

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

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

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

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

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

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

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

Редактировать:

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

Ответ:

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

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

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

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