Существует несколько вопросов о переполнении стека в духе «почему я не могу инициализировать статические члены-данные в классе в C ++». Большинство ответов цитаты из стандарта, говорящего вам какие ты можешь сделать; те, которые пытаются ответить Зачем обычно указывают на ссылку (теперь, казалось бы, недоступную) [РЕДАКТИРОВАТЬ: на самом деле она доступна, см. ниже] на сайте Страуструпа, где он заявляет, что разрешение инициализации статических членов в классе будет нарушать Правило единого определения (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 для статических членов шаблонных классов, почему он не может сделать это и для не шаблонных классов?
РЕДАКТИРОВАТЬ: Ответ на часто задаваемые вопросы по Stroustrup доступен Вот. Соответствующее предложение:
Однако, чтобы избежать сложных правил компоновщика, C ++ требует, чтобы у каждого объекта было уникальное определение. Это правило было бы нарушено, если бы C ++ допускал в своем классе определение сущностей, которые должны были храниться в памяти как объекты
Однако, похоже, что эти «сложные правила компоновщика» существуют и используются в случае шаблона, так почему бы и в простом случае тоже?
Хорошо, этот следующий пример кода демонстрирует разницу между сильной и слабой ссылкой на линкер. После я попытаюсь объяснить, почему переключение между двумя этими параметрами может привести к изменению исполняемого файла, созданного компоновщиком.
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?
Слабая ссылка не определено потому что это зависит от порядка ссылок, что делает его в лучшем случае неясным и неконтролируемым в зависимости от компоновщика. Это, вероятно, приемлемо, потому что нередко не хранить все шаблоны в одном файле, поскольку для работы компиляции требуются и прототип, и реализация.
Однако для членов статических данных класса, поскольку они были исторически сильными ссылками, и их нельзя было определить в объявлении, было правилом, и теперь, по крайней мере, обычной практикой является полное объявление данных с сильной ссылкой в файле реализации.
Фактически, из-за того, что компоновщик генерирует ошибки ссылок ODR для нарушений сильных ссылок, было обычной практикой иметь несколько объектных файлов (связанных блоков компиляции), которые были связаны условно, чтобы изменить поведение для различных комбинаций аппаратного и программного обеспечения и иногда для преимущества оптимизации. Зная, что если вы допустили ошибку в параметрах ссылки, вы получите сообщение о том, что вы забыли выбрать специализацию (без строгой ссылки) или выбрали несколько специализаций (несколько сильных ссылок)
Вы должны помнить, что во время введения C ++ 8-битные, 16-битные и 32-битные процессоры были все еще допустимыми целями, AMD и Intel имели схожие, но разные наборы инструкций, производители оборудования предпочитали закрытые частные интерфейсы открытым стандартам. И цикл сборки может занять часы, дни, даже неделю.
Структура сборки C ++ была довольно простой.
Компилятор строил объектные файлы, которые обычно содержали одну реализацию класса.
Затем компоновщик объединил все объектные файлы вместе в исполняемый файл.
Правило «Одно определение» относится к требованию, чтобы каждая переменная (и функция), используемая в исполняемом файле, появлялась только в одном объектном файле, созданном компилятором. Все остальные объектные файлы просто имеют внешние прототипные ссылки на переменную / функцию.
Шаблоны с очень поздним дополнением к C ++ и требуют, чтобы все детали реализации шаблона были доступны во время каждой компиляции каждого объекта, чтобы компилятор мог выполнять все свои оптимизации — это включает в себя много встраивания и еще большее искажение имен.
Я надеюсь, что это отвечает на ваш вопрос, потому что это является причиной правила ODR и почему оно не влияет на шаблоны. Поскольку компоновщик почти не имеет ничего общего с шаблонами, все они управляются компилятором. Исключением был случай использования специализации шаблона для помещения всего расширения шаблона в один объектный файл, поэтому его можно использовать в других объектных файлах, если они видят только прототипы для шаблона.
Редактировать:
В давние времена компоновщики часто связывали объектные файлы, созданные на разных языках. Распространено было связывать ASM и C, и даже после C ++ часть этого кода все еще использовалась, и это абсолютно необходимо для ODR. Тот факт, что ваш проект связан только с файлами C ++, не означает, что это все, что может сделать компоновщик, и поэтому он не будет изменен, потому что большинство проектов теперь являются исключительно C ++. Даже сейчас многие драйверы устройств используют компоновщик в соответствии с его более оригинальным намерением.
Ответ:
Однако кажется, что эти «сложные правила компоновщика» существуют и
используются в шаблонном случае, так почему бы и в простом случае тоже?
Компилятор управляет случаями шаблона и просто создает слабые ссылки компоновщика.
Линкер имеет ничего такого делать с шаблонами, это шаблоны, используемые компилятор для создания кода он передается компоновщику.
Таким образом, на правила компоновщика не влияют шаблоны, но правила компоновщика все еще важны, потому что ODR является требованием ASM и C, которые по-прежнему связывает компоновщик, и люди, не являющиеся вами, все еще фактически используют.