Подтвердить что ты не робот

Множественная ошибка определения, включая заголовочный файл С++ со встроенным кодом из нескольких источников

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

// example.h
#ifndef EXAMPLE_H_
#define EXAMPLE_H_
namespace test_ns{

class TestClass{
public:
    void testMethod();
};

void TestClass::testMethod(){
    // some code here...
}

} // end namespace test_ns
#endif

Если внутри одного и того же проекта я включаю этот заголовок из более чем одного файла cpp, я получаю сообщение об ошибке "multiple definition of test_ns::TestClass::testMethod()", а если я поместил определение метода внутри тела класса, это не произойдет:

// example.h
#ifndef EXAMPLE_H_
#define EXAMPLE_H_
namespace test_ns{

class TestClass{
public:
    void testMethod(){
        // some code here...
    }
};

} // end namespace test_ns
#endif

Так как класс определен внутри пространства имен, не должны ли эти две формы быть эквивалентными? Почему в первом случае метод считается дважды определенным?

4b9b3361

Ответ 1

Они не эквивалентны. Второй приведенный пример имеет неявный "встроенный" модификатор метода, и поэтому компилятор будет согласовывать несколько определений (наиболее вероятно с внутренней связью метода, если он не является строковым).

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

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

#ifndef EXAMPLE_H
#define EXAMPLE_H

//define your class here

#endif

Ответ 2

Внутри тела класса считается встроенным компилятором. Если вы реализуете вне тела, но все еще в заголовке, вы должны явно указывать метод как "inline".

namespace test_ns{

class TestClass{
public:
    inline void testMethod();
};

void TestClass::testMethod(){
    // some code here...
}

} // end namespace test_ns

Edit

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

Поэтому, когда метод, который не объявлен inline, реализован в файле заголовка, компилятор может в конечном итоге увидеть void TestClass:: testMethod() {...} десятки раз в предварительно обработанном файле. Теперь вы можете видеть, что это не имеет смысла, тот же эффект, что и при копировании/вставке его несколько раз в один исходный файл. И даже если вам удалось получить его только один раз в каждом модуле компиляции, используя некоторую форму условной компиляции (например, используя скобки включения), компоновщик все равно найдет этот символ метода в нескольких скомпилированных единицах (объектных файлах).

Ответ 3

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

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

Ответ 4

На самом деле возможно иметь определения в одном файле заголовка (без отдельного файла .c/.cpp) и по-прежнему иметь возможность использовать его из нескольких исходных файлов.

Рассмотрим этот заголовок foobar.h:

#ifndef FOOBAR_H
#define FOOBAR_H

/* write declarations normally */
void foo();
void bar();

/* use conditional compilation to disable definitions when necessary */
#ifndef ONLY_DECLARATIONS
void foo() {
   /* your code goes here */
}
void bar() {
   /* your code goes here */
}
#endif /* ONLY_DECLARATIONS */
#endif /* FOOBAR_H */

Если вы используете этот заголовок только в одном исходном файле, включите его и используйте его в обычном режиме. Как в main.c:

#include "foobar.h"

int main(int argc, char *argv[]) {
    foo();
}

Если в проекте есть другие исходные файлы, для которых требуется foobar.h, а затем #define ONLY_DECLARATIONS макрос, прежде чем включать его. В use_bar.c вы можете написать:

#define ONLY_DECLARATIONS
#include "foobar.h"

void use_bar() {
    bar();
}

После компиляции use_bar.o и main.o могут быть связаны вместе без ошибок, потому что только один из них (main.o) будет иметь реализацию foo() и bar().

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

Ответ 5

Ваш первый фрагмент кода падает на С++ "Одно правило определения" - см. здесь ссылку на статью в Википедии, описывающую ODR. Вы на самом деле падаете с точки 2, потому что каждый раз, когда компилятор включает файл заголовка в исходный файл, вы рискуете, что компилятор генерирует глобально видимое определение test_ns::TestClass::testMethod(). И, конечно, к тому времени, когда вы свяжете код, у компоновщика будут котята, потому что он найдет тот же символ в нескольких объектных файлах.

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

Вы можете добиться аналогичного эффекта в своем первом фрагменте кода, префикс TestClass::testMethod() с помощью inline.

Ответ 6

//Baseclass.h  or  .cpp

#ifndef CDerivedclass
#include "Derivedclass.h"
#endif

or
//COthercls.h    or .cpp

#ifndef CCommonheadercls
#include "Commonheadercls.h"
#endif

I think this suffice all instances.