Для чего нужны встроенные пространства имен?
C++11 позволяет inline namespace
s, все члены которого также автоматически вмещаются namespace
, Я не могу придумать какое-либо полезное применение этого - может кто-нибудь дать краткий, краткий пример ситуации, когда inline namespace
нужен и где это самое идиоматическое решение?
(Кроме того, мне не ясно, что происходит, когда namespace
объявлен inline
в одном, но не во всех объявлениях, которые могут находиться в разных файлах. Разве это не попрошайничество?)
6 ответов
Встроенные пространства имен - это функция управления версиями библиотеки, похожая на управление версиями символов, но реализованная исключительно на уровне C++11 (т. Е. Кросс-платформенный), а не является функцией определенного двоичного исполняемого формата (т. Е. Специфичной для платформы).
Это механизм, с помощью которого автор библиотеки может выглядеть как вложенное пространство имен и действовать так, как если бы все его объявления были в окружающем пространстве имен (встроенные пространства имен могут быть вложенными, поэтому "более вложенные" имена просачиваются вплоть до первого -inline пространство имен и выглядят и действуют так, как если бы их объявления были в любом из пространств имен между ними).
В качестве примера рассмотрим реализацию STL vector
, Если у нас были встроенные пространства имен с начала C++, то в C++98 заголовок <vector>
могло бы выглядеть так:
namespace std {
#if __cplusplus < 1997L // pre-standard C++
inline
#endif
namespace pre_cxx_1997 {
template <class T> __vector_impl; // implementation class
template <class T> // e.g. w/o allocator argument
class vector : __vector_impl<T> { // private inheritance
// ...
};
}
#if __cplusplus >= 1997L // C++98/03 or later
// (ifdef'ed out b/c it probably uses new language
// features that a pre-C++98 compiler would choke on)
# if __cplusplus == 1997L // C++98/03
inline
# endif
namespace cxx_1997 {
// std::vector now has an allocator argument
template <class T, class Alloc=std::allocator<T> >
class vector : pre_cxx_1997::__vector_impl<T> { // the old impl is still good
// ...
};
// and vector<bool> is special:
template <class Alloc=std::allocator<bool> >
class vector<bool> {
// ...
};
};
#endif // C++98/03 or later
} // namespace std
В зависимости от стоимости __cplusplus
либо один, либо другой vector
реализация выбрана. Если ваша кодовая база была написана в пре-C++98 раз, и вы обнаружили, что версия C++98 vector
создает проблемы для вас, когда вы обновляете свой компилятор, "все", что вам нужно сделать, это найти ссылки на std::vector
в вашей кодовой базе и заменить их на std::pre_cxx_1997::vector
,
Приходите к следующему стандарту, и поставщик STL просто повторяет процедуру снова, представляя новое пространство имен для std::vector
с emplace_back
поддержка (которая требует C++11) и включение этого, если __cplusplus == 201103L
,
Хорошо, так зачем мне для этого нужна новая языковая функция? Я уже могу сделать следующее, чтобы иметь тот же эффект, нет?
namespace std {
namespace pre_cxx_1997 {
// ...
}
#if __cplusplus < 1997L // pre-standard C++
using namespace pre_cxx_1997;
#endif
#if __cplusplus >= 1997L // C++98/03 or later
// (ifdef'ed out b/c it probably uses new language
// features that a pre-C++98 compiler would choke on)
namespace cxx_1997 {
// ...
};
# if __cplusplus == 1997L // C++98/03
using namespace cxx_1997;
# endif
#endif // C++98/03 or later
} // namespace std
В зависимости от стоимости __cplusplus
Я получаю одну или другую реализацию.
И ты был бы почти прав.
Рассмотрим следующий допустимый пользовательский код C++98 (было разрешено полностью специализировать шаблоны, которые живут в пространстве имен std
в C++98 уже):
// I don't trust my STL vendor to do this optimisation, so force these
// specializations myself:
namespace std {
template <>
class vector<MyType> : my_special_vector<MyType> {
// ...
};
template <>
class vector<MyOtherType> : my_special_vector<MyOtherType> {
// ...
};
// ...etc...
} // namespace std
Это совершенно правильный код, где пользователь предоставляет свою собственную реализацию вектора для набора типов, где он, очевидно, знает более эффективную реализацию, чем та, что найдена в (его копии) STL.
Но: когда вы специализируете шаблон, вы должны сделать это в пространстве имен, в котором он был объявлен. Стандарт говорит, что vector
объявлен в пространстве имен std
так что именно здесь пользователь по праву ожидает специализировать тип.
Этот код работает с не версионным пространством имен std
или с помощью встроенного пространства имен C++11, но не с помощью трюка с версиями, который использовался using namespace <nested>
потому что это раскрывает детали реализации, что истинное пространство имен, в котором vector
был определен не был std
непосредственно.
Есть и другие дыры, с помощью которых вы можете обнаружить вложенное пространство имен (см. Комментарии ниже), но встроенные пространства имен закрывают их все. И это все, что нужно сделать. Чрезвычайно полезно для будущего, но AFAIK Стандарт не предписывает встроенные имена пространств имен для своей собственной стандартной библиотеки (хотя я бы хотел, чтобы это было ошибочно), поэтому его можно использовать только для сторонних библиотек, но не сам стандарт (если поставщики компиляторов не договорились о схеме именования).
http://www.stroustrup.com/C++11FAQ.html (документ, написанный и поддерживаемый Бьярном Страуструпом, который, как вы думаете, должен знать о большинстве мотивов для большинства функций C++ 11.)
В соответствии с этим, это разрешить управление версиями для обратной совместимости. Вы определяете несколько внутренних пространств имен и создаете самое новое inline
, Или, во всяком случае, по умолчанию для людей, которые не заботятся о версиях. Я полагаю, что самой последней из них может быть будущая или передовая версия, которая еще не установлена по умолчанию.
Пример приведен ниже:
// file V99.h:
inline namespace V99 {
void f(int); // does something better than the V98 version
void f(double); // new feature
// ...
}
// file V98.h:
namespace V98 {
void f(int); // does something
// ...
}
// file Mine.h:
namespace Mine {
#include "V99.h"
#include "V98.h"
}
#include "Mine.h"
using namespace Mine;
// ...
V98::f(1); // old version
V99::f(1); // new version
f(1); // default version
Я не сразу понимаю, почему вы не ставите using namespace V99;
внутри пространства имен Mine
Но мне не нужно полностью понимать сценарий использования, чтобы поверить на слово Бьярне о мотивации комитета.
В дополнение ко всем ответам выше.
Встроенное пространство имен может использоваться для кодирования информации ABI или версии функций в символах. По этой причине они используются для обеспечения обратной совместимости ABI. Встроенные пространства имен позволяют вводить информацию в искаженное имя (ABI) без изменения API, поскольку они влияют только на имя символа компоновщика.
Рассмотрим этот пример:
Предположим, вы пишете функцию Foo
который принимает ссылку на объект, скажем bar
и ничего не возвращает.
Скажи в main.cpp
struct bar;
void Foo(bar& ref);
Если вы проверите имя вашего символа для этого файла после компиляции его в объект.
$ nm main.o
T__ Z1fooRK6bar
Имя символа компоновщика может отличаться, но оно наверняка будет где-то кодировать имя функции и тип аргумента.
Теперь это может быть bar
определяется как:
struct bar{
int x;
#ifndef NDEBUG
int y;
#endif
};
В зависимости от типа сборки, bar
может ссылаться на два разных типа / макета с одинаковыми символами компоновщика.
Чтобы предотвратить такое поведение, мы обертываем нашу структуру bar
во встроенное пространство имен, где в зависимости от типа сборки символ компоновщика bar
будет другим.
Итак, мы могли бы написать:
#ifndef NDEBUG
inline namespace rel {
#else
inline namespace dbg {
#endif
struct bar{
int x;
#ifndef NDEBUG
int y;
#endif
};
}
Теперь, если вы посмотрите на файл объекта каждого объекта, вы создадите один, используя release, а другой - с флагом отладки. Вы обнаружите, что символы компоновщика включают в себя имя встроенного пространства имен. В этом случае
$ nm rel.o
T__ ZROKfoo9relEbar
$ nm dbg.o
T__ ZROKfoo9dbgEbar
Имена символов компоновщика могут быть разными.
Обратите внимание на наличие rel
а также dbg
в именах символов.
Теперь, если вы попытаетесь связать отладку с режимом выпуска или наоборот, вы получите ошибку компоновщика, в отличие от ошибки времени выполнения.
Итак, чтобы резюмировать основные моменты, using namespace v99
а также inline namespace
были разными, первый был обходным путем для библиотек версий до того, как в C++11 было введено специальное ключевое слово (inline), которое исправляло проблемы использования using
, обеспечивая при этом те же функции управления версиями. С помощьюusing namespace
раньше вызывать проблемы с ADL (хотя теперь ADL, похоже, следует using
директивы), а внешняя специализация библиотечного класса / функции и т. д. пользователем не будет работать, если будет выполняться за пределами истинного пространства имен (чье имя пользователь не знал и не должен был знать, т.е. необходимо использовать B::abi_v2::, а не просто B:: для разрешения специализации).
//library code
namespace B { //library name the user knows
namespace A { //ABI version the user doesn't know about
template<class T> class myclass{int a;};
}
using namespace A; //pre inline-namespace versioning trick
}
// user code
namespace B { //user thinks the library uses this namespace
template<> class myclass<int> {};
}
Появится предупреждение статического анализа. first declaration of class template specialization of 'myclass' outside namespace 'A' is a C++11 extension [-Wc++11-extensions]
. Но если вы сделаете пространство имен A встроенным, то компилятор правильно определит специализацию. Хотя с расширениями C++11 проблема исчезает.
Построчные определения не разрешаются при использовании using
; они должны быть объявлены во вложенном / невложенном блоке пространства имен расширений (что означает, что пользователю необходимо снова узнать версию ABI, если по какой-либо причине ему было разрешено предоставить свою собственную реализацию функции).
#include <iostream>
namespace A {
namespace B{
int a;
int func(int a);
template<class T> class myclass{int a;};
class C;
extern int d;
}
using namespace B;
}
int A::d = 3; //No member named 'd' in namespace A
class A::C {int a;}; //no class named 'C' in namespace 'A'
template<> class A::myclass<int> {}; // works; specialisation is not an out-of-line definition of a declaration
int A::func(int a){return a;}; //out-of-line definition of 'func' does not match any declaration in namespace 'A'
namespace A { int func(int a){return a;};} //works
int main() {
A::a =1; // works; not an out-of-line definition
}
Проблема исчезает, если сделать B встроенным.
Другой функционал inline
пространства имен позволяют создателю библиотеки предоставлять прозрачное обновление библиотеки 1) без принуждения пользователя к рефакторингу кода с новым именем пространства имен и 2) предотвращение отсутствия подробностей и 3) обеспечение абстракции деталей, не относящихся к API, в то время как 4) предоставление той же полезной диагностики и поведения компоновщика, что и при использовании не встроенного пространства имен. Допустим, вы используете библиотеку:
namespace library {
inline namespace abi_v1 {
class foo {
}
}
}
Это позволяет пользователю позвонить library::foo
без необходимости знать или включать версию ABI в документацию, что выглядит чище. С помощьюlibrary::abiverison129389123::foo
будет выглядеть грязно.
Когда выполняется обновление foo
, т.е. добавление нового члена в класс, это не повлияет на существующие программы на уровне API, потому что они уже не будут использовать член И изменение имени встроенного пространства имен ничего не изменит на уровне API, потому что library::foo
все равно будет работать.
namespace library {
inline namespace abi_v2 {
class foo {
//new member
}
}
}
Однако для программ, которые связываются с ним, поскольку имя встроенного пространства имен преобразовано в имена символов, как обычное пространство имен, изменение не будет прозрачным для компоновщика. Следовательно, если приложение не перекомпилировано, но связано с новой версией библиотеки, оно будет представлять символabi_v1
ошибка не найдена, а не происходит фактического связывания, а затем вызывает загадочную логическую ошибку во время выполнения из-за несовместимости с ABI. Добавление нового члена приведет к совместимости ABI из-за изменения определения типа, даже если это не повлияет на программу во время компиляции (уровень API).
В этом сценарии:
namespace library {
namespace abi_v1 {
class foo {
}
}
inline namespace abi_v2 {
class foo {
//new member
}
}
}
Подобно использованию двух не встроенных пространств имен, он позволяет связывать новую версию библиотеки без необходимости перекомпилировать приложение, потому что abi_v1
будет искажен в одном из глобальных символов и будет использовать правильное (старое) определение типа. Однако повторная компиляция приложения приведет к тому, что ссылки будут преобразованы вlibrary::abi_v2
.
С помощью using namespace
менее функционально, чем использование inline
(в том, что определения вне строки не разрешаются), но дает те же 4 преимущества, что и выше. Но реальный вопрос заключается в том, зачем продолжать использовать обходной путь, когда для этого теперь есть специальное ключевое слово. Это лучшая практика, менее многословная (нужно изменить 1 строку кода вместо 2) и проясняет намерение.
На самом деле я обнаружил другое использование встроенных пространств имен.
С Qt вы получаете дополнительные, приятные функции, используя Q_ENUM_NS
что, в свою очередь, требует, чтобы в пространстве имен имелся мета-объект, который объявлен с Q_NAMESPACE
, Однако для того, чтобы Q_ENUM_NS
на работу, должен быть соответствующий Q_NAMESPACE
в том же файле ⁽¹⁾. И может быть только один, или вы получите повторяющиеся ошибки определения. Фактически это означает, что все ваши перечисления должны быть в одном заголовке. Тьфу.
Или... вы можете использовать встроенные пространства имен. Скрытие перечислений в inline namespace
приводит к тому, что метаобъекты имеют разные искаженные имена, а пользователи считают, что дополнительное пространство имен не существует ".
Таким образом, они полезны для разбиения материала на несколько подпространств имен, которые все выглядят как одно пространство имен, если вам нужно сделать это по какой-то причине. Конечно, это похоже на написание using namespace inner
во внешнем пространстве имен, но без СУХОГО нарушения записи имени внутреннего пространства имен дважды.
Это на самом деле хуже, чем это; это должно быть в том же наборе скобок.
Если вы не попытаетесь получить доступ к мета-объекту без полной его квалификации, но мета-объект вряд ли когда-либо будет использоваться напрямую.
Встроенные пространства имен также можно использовать для предоставления мелкозернистого доступа к функциям/именам в пространствах имен.
Это используется вstd::literals
.
literals
пространства имен в std являются встроенными пространствами имен, так что:
- если вы используете
using namespace std;
где-то вы также получаете доступ ко всем литералам, определяемым пользователем, в std. - но если вам просто нужен набор udl в вашем локальном коде, вы также можете сделать
using namespace std::literals::string_literals;
и вы просто получите символы udl, определенные в этом пространстве имен.
Это кажется полезным методом для символов, к которым вы хотели бы получить неквалифицированный доступ (udl, операторы и т. д.), где вы можете просто связать их во встроенном пространстве имен, чтобы вместо этого вы могли использовать конкретное использование только этого (под-) пространства имен. пространства имен всей библиотеки.