Безопасно ли связывать объекты С++ 17, С++ 14 и С++ 11

Предположим, у меня есть три скомпилированных объекта, все они созданы одним и тем же компилятором/версией:

  1. A был скомпилирован со стандартом С++ 11
  2. B был составлен с использованием стандарта С++ 14
  3. C был скомпилирован со стандартом С++ 17

Для простоты предположим, что все заголовки были написаны на С++ 11, используя только конструкции, семантика которых не изменилась между всеми тремя стандартными версиями, и поэтому любые взаимозависимости были правильно выражены с включением заголовка, и компилятор не возражал.

Какие комбинации этих объектов есть и не безопасно ли ссылаться на один бинарный файл? Зачем?


EDIT: ответы на основные компиляторы (например, gcc, clang, vs++) приветствуются

Ответ 1

Какие комбинации этих объектов есть и не безопасно ли ссылаться на один бинарный файл? Зачем?

Для GCC безопасно связывать любую комбинацию объектов A, B и C. Если все они построены с той же версией, то они совместимы с ABI, стандартная версия (то есть опция -std) не делает никаких разница.

Зачем? Потому что это важное свойство нашей реализации, над которым мы много работаем.

Там, где у вас проблемы, вы связываете объекты, скомпилированные с разными версиями GCC, и вы использовали неустойчивые функции из нового стандарта C++ до завершения поддержки GCC для этого стандарта. Например, если вы скомпилируете объект с использованием GCC 4.9 и -std=C++11 а другой объект с GCC 5 и -std=C++11 вас возникнут проблемы. Поддержка C++ 11 была экспериментальной в GCC 4.x, и поэтому между версиями C++ 11 GCC 4.9 и 5 были несовместимые изменения. Аналогично, если вы скомпилируете один объект с GCC 7 и -std=C++17 а другой объект с GCC 8 и -std=C++17 вас возникнут проблемы, поскольку поддержка C++ 17 в GCC 7 и 8 все еще экспериментально и эволюционирует.

С другой стороны, любая комбинация следующих объектов будет работать (хотя см. Примечание ниже о libstdC++.so):

  • объект D, скомпилированный с помощью GCC 4.9 и -std=C++03
  • объект E, скомпилированный с помощью GCC 5 и -std=C++11
  • объект F, скомпилированный с помощью GCC 7 и -std=C++17

Это связано с тем, что поддержка C++ 03 стабильна во всех трех версиях компилятора, поэтому компоненты C++ 03 совместимы между всеми объектами. C++ 11 поддерживается с GCC 5, но объект D не использует никаких функций C++ 11, а объекты E и F используют версии, где поддержка C++ 11 стабильна. Поддержка C++ 17 нестабильна ни в одной из используемых версий компилятора, но только объект F использует функции C++ 17, и поэтому нет проблем с совместимостью с двумя другими объектами (единственные функции, которые они используют, принадлежат C++ 03 или C++ 11, а используемые версии делают эти части ОК). Если позже вы захотите скомпилировать четвертый объект G, используя GCC 8 и -std=C++17 тогда вам нужно будет перекомпилировать F с той же версией (или не ссылкой на F), потому что символы C++ 17 в F и G несовместимы.

Единственное предостережение о совместимости, описанное выше между D, E и F, заключается в том, что ваша программа должна использовать libstdC++.so от GCC 7 (или более поздней). Поскольку объект F был скомпилирован с помощью GCC 7, вам нужно использовать общую библиотеку из этой версии, поскольку компиляция любой части программы с помощью GCC 7 может приводить к зависимостям от символов, которые отсутствуют в libstdC++.so из GCC 4.9 или GCC 5. Аналогичным образом, если вы связаны с объектом G, построенным с помощью GCC 8, вам нужно будет использовать libstdC++.so из GCC 8, чтобы убедиться, что все символы, необходимые для G, найдены. Простое правило состоит в том, чтобы обеспечить совместную библиотеку, которую использует программа во время выполнения, по крайней мере такая же новая, как версия, используемая для компиляции любого из объектов.

Еще одно предостережение при использовании GCC, уже упоминавшееся в комментариях к вашему вопросу, состоит в том, что поскольку GCC 5 имеет две версии std::string доступные в libstd C++. Эти две реализации не совместимы с ссылками (у них разные искаженные имена, поэтому они не могут быть связаны друг с другом), но могут сосуществовать в одном и том же двоичном файле (они имеют разные искаженные имена, поэтому не конфликтуют, если один объект использует std::string а другой использует std::__cxx11::string). Если ваши объекты используют std::string то обычно они должны быть скомпилированы с той же строковой реализацией. Скомпилируйте с -D_GLIBCXX_USE_CXX11_ABI=0 чтобы выбрать оригинальную реализацию, gcc4-compatible, или -D_GLIBCXX_USE_CXX11_ABI=1 чтобы выбрать новую реализацию cxx11 (не обманывайте ее именем, ее также можно использовать в C++ 03, она называемый cxx11 поскольку он соответствует требованиям C++ 11). Какая реализация по умолчанию зависит от того, как настроен GCC, но значение по умолчанию всегда можно переопределить во время компиляции с помощью макроса.

Ответ 2

В ответе есть две части. Совместимость на уровне компилятора и совместимость на уровне компоновщика. Начнем с первого.

допустим, что все заголовки были написаны в C++ 11

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

Тем не менее, если параметры компилятора, используемые для компиляции единицы перевода, указывают конкретный стандарт C++, то любые функции, доступные только в более новых стандартах, не должны быть доступны. Это делается с __cplusplus директивы __cplusplus. Посмотрите векторный исходный файл для интересного примера того, как он используется. Аналогично, компилятор отклонит любые синтаксические функции, предлагаемые более новыми версиями стандарта.

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

C.3.1 Пункт 2: лексические соглашения

Одиночные кавычки ограничивают символьный литерал в C++ 11, тогда как они являются разделителями цифр в C++ 14 и C++ 17. Предположим, у вас есть следующее определение макроса в одном из чистых файлов заголовков C++ 11:

#define M(x, ...) __VA_ARGS__

// Maybe defined as a field in a template or a type.
int x[2] = { M(1'2,3'4) };

Рассмотрим две единицы перевода, которые включают файл заголовка, но целевые C++ 11 и C++ 14, соответственно. При таргетинге на C++ 11 запятая внутри кавычек не считается разделителем параметров; есть только один параметр. Следовательно, код будет эквивалентен:

int x[2] = { 0 }; // C++11

С другой стороны, при таргетинге на C++ 14 одиночные кавычки интерпретируются как разделители цифр. Следовательно, код будет эквивалентен:

int x[2] = { 34, 0 }; // C++14 and C++17

Дело здесь в том, что использование одиночных кавычек в одном из чистых файлов заголовков C++ 11 может привести к неожиданным ошибкам в единицах перевода, которые нацелены на C++ 14/17. Поэтому, даже если заголовочный файл написан в C++ 11, его необходимо тщательно записать, чтобы убедиться, что он совместим с более поздними версиями стандарта. Здесь может быть полезной директива __cplusplus.

В других трех положениях стандарта:

C.3.2 Пункт 3: основные понятия

Изменить: новый обычный (без размещения) деллалокатор

Обоснование: требуется для определения размера освобождения.

Влияние на оригинальную функцию: Действительный код C++ 2011 может объявить глобальную функцию распределения мест и функцию освобождения следующим образом:

void operator new(std::size_t, std::size_t); 
void operator delete(void*, std::size_t) noexcept;

Однако в этом Международном стандарте объявление удаления оператора может совпадать с предопределенным обычным (без размещения) оператором delete (3.7.4). Если это так, программа плохо сформирована, как это было для функций распределения членов класса и функций освобождения (5.3.4).

C.3.3 Пункт 7: декларации

Изменить: constexpr нестатические функции-члены не являются неявно функциями-членами-константами.

Обоснование: необходимо, чтобы функции-члены constexpr могли мутировать объект.

Влияние на оригинальную функцию: действительный код C++ 2011 может не скомпилироваться в этом международном стандарте.

Например, следующий код действителен в C++ 2011, но недействителен в этом Международном стандарте, поскольку он дважды объявляет одну и ту же функцию-член с различными типами возврата:

struct S {
constexpr const int &f();
int &f();
};

C.3.4 Статья 27: библиотека ввода/вывода

Изменить: получение не определено.

Обоснование: использование get считается опасным.

Влияние на оригинальную функцию: Действительный код C++ 2011, который использует функцию gets, может не скомпилироваться в этом Международном стандарте.

Потенциальные несовместимости между C++ 14 и C++ 17 обсуждаются в С.4. Поскольку все нестандартные файлы заголовков записаны в C++ 11 (как указано в вопросе), эти проблемы не возникнут, поэтому я не буду упоминать их здесь.

Теперь я обсужу совместимость на уровне компоновщика. В целом потенциальные причины несовместимости включают следующее:

Если формат результирующего объектного файла зависит от стандартного целевого стандарта C++, компоновщик должен иметь возможность связывать разные объектные файлы. В GCC, LLVM и V C++ это, к счастью, не так. То есть формат файлов объектов одинаковый независимо от целевого стандарта, хотя он сильно зависит от самого компилятора. Фактически, ни один из компоновщиков GCC, LLVM и V C++ не нуждается в знаниях о стандарте C++. Это также означает, что мы можем связать файлы объектов, которые уже скомпилированы (статическая привязка среды выполнения).

Если программа запуска программы (функция, вызывающая main) отличается для разных C++ стандартов, а разные подпрограммы несовместимы друг с другом, тогда было бы невозможно связать файлы объектов. В GCC, LLVM и V C++ это, к счастью, не так. Кроме того, подпись main функции (и ограничений, применяемых к ней, см. Раздел 3.6 стандарта) одинакова во всех C++ стандартах, поэтому не имеет значения, в какой единицы перевода она существует.

В общем, WPO может плохо работать с объектными файлами, скомпилированными с использованием разных C++ стандартов. Это зависит от того, какие этапы компилятора требуют знания целевого стандарта и каких этапов нет, и того влияния, которое оно оказывает на межпроцедурные оптимизации, которые пересекают объектные файлы. К счастью, GCC, LLVM и V C++ хорошо разработаны и не имеют этой проблемы (не то, что я знаю).

Поэтому GCC, LLVM и V C++ были разработаны для обеспечения совместимости двоичных файлов в разных версиях стандарта C++. На самом деле это не требование самого стандарта.

Кстати, хотя компилятор V C++ предлагает STD-переключатель, который позволяет настроить таргетинг на определенную версию стандарта C++, он не поддерживает таргетинг C++ 11. Минимальная версия, которая может быть указана, - C++ 14, которая по умолчанию начинается с Visual C++ 2013 Обновление 3. Вы можете использовать более старую версию V C++ для C++ 11, но тогда у вас будет использовать разные компиляторы V C++ для компиляции различных единиц перевода, предназначенных для разных версий стандарта C++, который, по крайней мере, сломает WPO.

CAVEAT: Мой ответ может быть неполным или очень точным.

Ответ 3

Новые C++ стандарты выпускаются в двух частях: языковые функции и стандартные библиотечные компоненты.

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

Но стандартная библиотека...

Каждая версия компилятора поставляется с реализацией стандартной библиотеки C++ (libstd C++ с gcc, lib C++ с clang, стандартной библиотекой MS C++ с V C++,...) и ровно одной реализацией, а не многие реализации для каждой стандартной версии. Также в некоторых случаях вы можете использовать другую реализацию стандартной библиотеки, чем предоставленный компилятором. Вы должны заботиться о том, чтобы связать реализацию более старой стандартной библиотеки с более новой.

Конфликт, который может возникнуть между сторонними библиотеками и вашим кодом, - это стандартная библиотека (и другие библиотеки), которая связана с этими сторонними библиотеками.