Хорошо, у меня есть 3 файла, заголовок, источник указанного заголовка и основной. В заголовочном файле я определяю класс с помощью функции. В исходном файле я определяю функцию. Но в главном файле я переопределить функции, а затем сделать экземпляр класса в главной функции и вызвать функцию. Это компилируется просто отлично — без предупреждения. И результат страшен, если не сказать больше.
Заголовок: testme.h
#ifndef testme_h_
#include <iostream>
using namespace std;
class wtf {
public:
string getStr();
};
#endif
Источник: testme.cpp
#include "testme.h"
string wtf::getStr() {
return "Hello World!";
};
Main: main.cpp
#include <iostream>
using namespace std;
#include "testme.h"
string wtf::getStr()
{
return "God is Dead.";
}
int main()
{
wtf f;
cout << f.getStr() << endl;
}
Выход:
God is Dead.
Почему это работает? Почему нет ошибки в отношении нескольких определений? Почему определение файла souce просто игнорируется? Почему нет предупреждений?
Частичный ответ
Когда это перекомпилируется как «g ++ main.cpp testme.cpp -o sanity.o», это фактически выдает ошибку компоновщика.
Однако меня поразило то, что этот небольшой кейс, который я написал, отражает проблему, которая возникает у меня в более крупной программе, в которой есть функция, определенная в библиотеке, и все же мы переопределяем функцию в другой программе «набора тестов» почти таким же образом. Почему это так? А как насчет того, что он в библиотеке позволяет переопределять ODR?
Почему это работает? Почему нет ошибки в отношении нескольких определений? Почему определение файла souce просто игнорируется? Почему нет предупреждений?
Частичный ответ Когда это перекомпилируется как «g ++ main.cpp testme.cpp -o sanity.o», это фактически выдает ошибку компоновщика.
В Дизайн и эволюция C ++, Страуструп говорит, что при разработке функции у него иногда будет выбор между (1) запутанным правилом, которое компилятор сможет применять с предупреждениями и сообщениями об ошибках, или (2) простым правилом, которое компилятор может не применять. во всех случаях. Он пытался выбрать простые правила, и он надеялся, что компиляторы в конечном итоге смогут применять / обнаруживать ошибки. Есть несколько частей C и C ++, которые, по сути, показывают, каков был уровень техники в 1970-х, 1980-х, 1990-х и т. Д. (Например, inline
, register
, volatile
). Там могло быть больше.
Грубо говоря, этот компромисс является источником неопределенного поведения в стандарте. Если простое правило не может быть применено (или комитет считает, что принудительное применение правила является дорогостоящим), правило остается в стандарте, а нарушения объявляются неопределенным поведением. Это ответственность программиста никогда не вызывать неопределенное поведение. Компиляторы и связанные инструменты могут применять некоторые из этих правил время от времени. Некоторые компиляторы или связанные с ними инструменты могут постоянно применять некоторые из этих правил. Но, в общем, ты сам по себе.
Дизайн и Эволюция даже включает обсуждение вашего конкретного вопроса. После написания компилятора Страуструп тоже не хотел писать компоновщик. Он придумал простой способ полагаться на системный компоновщик, но в первые дни многие компоновщики имели жесткие ограничения на длину символов. Он много работал, чтобы убедить людей, написавших системные компоновщики, изменить их настолько, чтобы они работали с Cfront. В конце концов он был успешным.
Сегодня компоновщик обычно поставляется с компилятором, но комитет по стандартизации все еще рассматривает его как отдельный инструмент, возможно, вне контроля людей, пишущих компилятор. Одно правило определения существует в основном для обеспечения совместимости выходных данных компилятора с большинством стандартных линкеров, но комитет не требует, чтобы эти линкеры могли выявлять нарушения ODR. Кроме того, программы могут компилироваться и связываться поэтапно, поэтому нет гарантии, что компоновщик когда-либо будет иметь достаточно информации для обнаружения нарушения ODR.
Стоит отметить, что много компиляторов сгенерирует несколько символов для inline
функции и template
функции, ожидая, что компоновщик выбросит избыточные определения. Так что компоновщик не обязательно видит несколько определений. (Что касается ODR, эти определения должны быть идентичны, чтобы компоновщик мог отбросить все, кроме одного.)
А как насчет того, что он в библиотеке позволяет переопределять ODR?
Я бы не сказал «переопределить». Это звучит так, будто поведение является намеренным. Я бы сказал «нарушать, не будучи обнаруженным». Ответ прост: «Стандартный комитет не ожидает, что компоновщик всегда обнаруживает нарушения, а многие компоновщики этого не делают». Вы должны избегать нарушений ODR. Однако вы не одиноки: пространства имен существуют частично, чтобы облегчить решение этой проблемы.
Других решений пока нет …