2012-05-20 5 views
19

Если два файла C++ имеют разные определения классов с тем же именем, тогда, когда они компилируются и связаны, что-то выкидывается даже без предупреждения. Например,Такое же имя класса в разных файлах на C++

// a.cc 
class Student { 
public: 
    std::string foo() { return "A"; } 
}; 
void foo_a() 
{ 
    Student stu; 
    std::cout << stu.foo() << std::endl; 
} 

// b.cc 
class Student { 
public: 
    std::string foo() { return "B"; } 
}; 
void foo_b() 
{ 
    Student stu; 
    std::cout << stu.foo() << std::endl; 
} 

После компиляции и связаны друг с другом с помощью G ++, как будет выводить «А» (если a.cc предшествует b.cc в порядке командной строки).

Аналогичная тема here. Я вижу, что пространство имен решит эту проблему, но я не знаю, почему компоновщик даже не снимает предупреждение. И если одно определение класса имеет дополнительную функцию, которая не определена в другой, скажем, если b.cc обновляется:

// b.cc 
class Student { 
public: 
    std::string foo() { return "B"; } 
    std::string bar() { return "K"; } 
}; 
void foo_b() 
{ 
    Student stu; 
    std::cout << stu.foo() << stu.bar() << std::endl; 
} 

Тогда stu.bar() работает хорошо. Спасибо всем, кто может рассказать мне, как работает компилятор и компоновщик в такой ситуации.

В качестве дополнительного вопроса, если классы определены в заголовочных файлах, должны ли они всегда быть обернуты безымянным пространством имен, чтобы избежать такой ситуации? Есть ли побочные эффекты?

+0

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

ответ

16

Это нарушение одно правило определения (C++ 03, «правило одно определение» 3.2/5) в, который говорит (среди прочего):

Там может быть больше, чем одно определение типа (9), ... в программе, при условии, что каждое определение отображается в другой блок перевода и при условии, что определения удовлетворяют следующим требованиям . С учетом такой объект с именем D определены в более чем одной единице перевода, то

  • каждое определение D состоит из одной и той же последовательности маркеров;

Если вы нарушили правила на одно определение, его поведение не определено (что означает, что могут происходить странные вещи).

Компилятор видит несколько определений Student::foo() - один в объектном файле и один в b. Однако он не жалуется на это; он просто выбирает один из двух (как это бывает, первый из них встречается). Эта «мягкая» обработка повторяющихся функций, по-видимому, происходит только для встроенных функций. Для не-встроенных функций компоновщик будет жалуется на несколько определений и откажется создать исполняемый файл (могут быть варианты, которые ослабляют это ограничение). И GNU ld, и компоновщик MSVC ведут себя таким образом.

Поведение имеет некоторый смысл; встроенные функции должны быть доступны в каждой единицы перевода, в которой они используются. И в общем случае они должны иметь доступ к не-встроенным версиям (в случае, если вызов не встроен или если адрес функции занят). inline - это просто бесплатный проход вокруг правила единого определения, но для его работы все встроенные определения должны быть одинаковыми.

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

Что касается вашего второго вопроса:

В качестве дополнительного вопроса, если классы определены в заголовочных файлах, должны они всегда будут обернуты с безымянного пространства имен, чтобы избежать такой ситуации? Есть ли побочные эффекты?

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

Поместите их в разные пространства имен с именами.

+0

Иные пространства имен также будут работать. Именованные пространства имен предпочтительнее, но, по крайней мере, он может исключить UB и заставить все работать, помещая 'namespace {}' вокруг кода нарушения. – Potatoswatter

+0

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

+0

Я обновил ответ с некоторыми подробностями о том, почему (и когда) компоновщик не жалуется на несколько определений некоторых функций. –

3

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

+0

На самом деле это не отвечает на вопрос: * почему * предупреждает ли линкер? –

+0

Линкер ничего не знает о C++, поэтому свяжет первое, что подходит. Он будет жаловаться только в том случае, если он не может найти символ или встречает контрастное определение. – Walter

0

Я думаю, что ваш «лишний» вопрос является ключом к основному вопросу.

Если я понимаю ваш дополнительный вопрос, то я думаю, что вы делаете не хотите обернуть их в пространство имен, так как если вы # включите один и тот же класс в несколько файлов .cc, то вы, вероятно, захотите использовать только одну копию каждого метода, даже если они определены внутри класса.

Этот (тип) объясняет, почему вы только получаете одну версию каждой функции в своем основном примере. Я ожидаю, что компоновщик просто предполагает, что две функции идентичны - генерируются из одного и того же #include источника. Было бы неплохо, если бы линкер обнаруживал, когда они были разными, и выдавал предупреждение, но я думаю, что это сложно.

Как указывает Марк Б, практический ответ: «Просто не ходи туда».

0

Помимо нарушения одного правила определения, вы не видите компилятор жалуясь из Name mangling in C++

Edit: Как отметил Konrad Rudolph: исковерканные имена в этом случае было бы то же самое.

+0

Я не вижу, как это связано с изменением имени. Вы можете объяснить? –

+0

ИМХО, поскольку определение класса в разных файлах, несмотря на одноименное имя, искаженные имена будут разными, поэтому сохраняется уникальность. Исправьте меня, если я ошибаюсь! –

+2

Вы ошибаетесь. Искаженные имена одинаковы. На самом деле, если бы они не были, у ОП не было бы проблем, которые у него есть. –

2

но я не знаю, почему линкер даже не снимает предупреждение.

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

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

Как следствие, линкер просто признает, что то, что вы бросаете на него, не нарушает ODR. Не идеальная ситуация, но хорошо.

+0

LOL, вы просто ответили на свой комментарий выше – Walter

+2

@Walter Очень проницательный. Это отсутствовало в других ответах, поэтому я решил написать ответ самостоятельно. –

+0

В некоторых системах вы должны использовать компоновщик, предоставляемый операционной системой. Вероятно, он ничего не знает о C++, поэтому он не может диагностировать языковые нарушения. Поэтому для обнаружения проблем допускается, но не требуется. –