Для чего нужны встроенные пространства имен?

334

C ++ 11 допускает inline namespaces, все члены которых также автоматически включаются в корпус namespace. Я не могу придумать какое-либо полезное применение этого - может ли кто-нибудь дать краткий, краткий пример ситуации, когда inline namespaceнужен и где это самое идиоматическое решение?

(Кроме того, мне не ясно, что происходит, когда a namespaceобъявляется inlineв одном, но не во всех объявлениях, которые могут находиться в разных файлах. Разве это не вызывает проблем?)

Вальтер
источник

Ответы:

339

Встроенные пространства имен - это функция управления версиями библиотеки, похожая на управление версиями символов , но реализованная исключительно на уровне 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) и вставляя этот iff __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 Стандарт не предписывает встроенные имена пространств имен для своей собственной стандартной библиотеки (хотя я бы хотел, чтобы это было ошибочно), поэтому его можно использовать только для сторонних библиотек, но не для сам стандарт (если поставщики компиляторов не договорились о схеме именования).

Марк Муц - ммц
источник
23
+1 за объяснение, почему using namespace V99;не работает в примере Страуструпа.
Стив Джессоп
3
И точно так же, если я начну новую реализацию C ++ 21 с нуля, то я не хочу быть обремененным реализацией множества старых глупостей std::cxx_11. Не каждый компилятор всегда будет реализовывать все старые версии стандартных библиотек, хотя в настоящее время соблазнительно думать, что было бы очень небольшим бременем потребовать, чтобы существующие реализации ушли в старое при добавлении новых, поскольку на самом деле все они в любом случае. Я полагаю, что то, что мог бы с пользой сделать стандарт, сделало его необязательным, но со стандартным именем, если оно есть.
Стив Джессоп
46
Это еще не все. ADL также был причиной (ADL не будет следовать с использованием директив), и поиск имени тоже. ( using namespace Aв пространстве имен B делает имена в пространстве имен B скрывать имена в пространстве имен A, если вы ищете B::name- не так с встроенными пространствами имен).
Йоханнес Шауб - лит
4
Почему бы просто не использовать ifdefs для полной реализации вектора? Все реализации будут находиться в одном пространстве имен, но только одна из них будет определена после предварительной обработки
sasha.sochka
6
@ sasha.sochka, потому что в этом случае вы не можете использовать другие реализации. Они будут удалены препроцессором. Со встроенными пространствами имен вы можете использовать любую реализацию, какую пожелаете, указав полное имя (или usingключевое слово).
Василий Бирюков
70

http://www.stroustrup.com/C++11FAQ.html#inline-namespace (документ, написанный и поддерживаемый Бьярном Страуструпом, который, как вы думаете, должен знать о большинстве мотивов для большинства функций 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, но мне не нужно полностью понимать сценарий использования, чтобы поверить на слово Бьярне о мотивации комитета.

Стив Джессоп
источник
Так на самом деле последняя f(1)версия будет вызываться из встроенного V99пространства имен?
Эйтан Т
1
@EitanT: да, потому что глобальное пространство имен имеет using namespace Mine;, а Mineпространство имен содержит все из встроенного пространства имен Mine::V99.
Стив Джессоп
2
@Walter: вы удаляете inlineиз файла V99.hв выпуске, который включает V100.h. Вы также можете одновременно изменить Mine.h, чтобы добавить дополнительное включение. Mine.hявляется частью библиотеки, а не частью клиентского кода.
Стив Джессоп
5
@walter: они не устанавливаются V100.h, они устанавливают библиотеку под названием «Mine». В версии 99 "Mine" есть 3 заголовочных файла Mine.h, V98.hи V99.h. Есть 4 файлов заголовка в версии 100 «Mine» - Mine.h, V98.h, V99.hи V100.h. Расположение заголовочных файлов - это деталь реализации, которая не имеет отношения к пользователям. Если они обнаруживают какую-то проблему совместимости, которая означает, что им нужно использовать конкретно Mine::V98::fиз некоторого или всего их кода, они могут смешивать вызовы Mine::V98::fиз старого кода с вызовами Mine::fво вновь создаваемом коде.
Стив Джессоп
2
@Walter Как упоминается в другом ответе, шаблоны должны быть специализированы для пространства имен, в котором они объявлены, а не для пространства имен, использующего то, в котором они объявлены. Хотя это выглядит странно, способ, которым это делается, позволяет специализировать шаблоны в Mineвместо того, чтобы специализироваться на Mine::V99или Mine::V98.
Джастин Тайм - Восстановить Монику
8

В дополнение ко всем остальным ответам.

Встроенное пространство имен может использоваться для кодирования информации 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в именах символов.

Теперь, если вы попытаетесь связать отладку с режимом выпуска или наоборот, вы получите ошибку компоновщика, в отличие от ошибки времени выполнения.

coder3101
источник
1
Да, это имеет смысл. Так что это больше для разработчиков библиотек и тому подобное.
Уолтер
3

На самом деле я обнаружил другое использование встроенных пространств имен.

С Qt вы получаете некоторые дополнительные полезные функции Q_ENUM_NS, которые, в свою очередь, требуют, чтобы в пространстве имен содержался мета-объект, который объявлен с помощью Q_NAMESPACE. Тем не менее, для того, Q_ENUM_NSчтобы работать, должен быть соответствующий Q_NAMESPACE в том же файле ⁽¹⁾. И может быть только один, или вы получите повторяющиеся ошибки определения. По сути, это означает, что все ваши перечисления должны быть в одном заголовке. Тьфу.

Или ... вы можете использовать встроенные пространства имен. Скрытие перечислений вinline namespaceмета-объектах приводит к тому, что метаобъекты имеют разные искаженные имена, а пользователи считают, что дополнительного пространства имен не существует ».

Таким образом, они полезны для разбиения материала на несколько подпространств имен, которые все выглядят как одно пространство имен, если вам нужно сделать это по какой-то причине. Конечно, это похоже на запись using namespace innerво внешнем пространстве имен, но без СУХОГО нарушения записи имени внутреннего пространства имен дважды.


  1. Это на самом деле хуже, чем это; это должно быть в том же наборе скобок.

  2. Если вы не попытаетесь получить доступ к мета-объекту без полной его квалификации, но мета-объект вряд ли когда-либо будет использоваться напрямую.

Мэтью
источник
Можете ли вы набросать это с помощью скелета кода? (в идеале без явной ссылки на Qt). Все это звучит довольно запутанно / неясно.
Уолтер
Не ... легко. Причина, по которой необходимы отдельные пространства имен, связана с деталями реализации Qt. TBH, трудно представить ситуацию вне Qt, которая бы имела такие же требования. Однако для этого Qt-специфичного сценария они чертовски полезны! См. Пример gist.github.com/mwoehlke-kitware/… или github.com/Kitware/seal-tk/pull/45 .
Мэтью
0

Таким образом, чтобы подвести итог основным пунктам, 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
        } 
    }
}

Подобно использованию 2 не встроенных пространств имен, он позволяет связывать новую версию библиотеки без необходимости перекомпиляции приложения, поскольку abi_v1он будет искажен в одном из глобальных символов и будет использовать правильное (старое) определение типа. Однако перекомпиляция приложения приведет к разрешению ссылок на library::abi_v2.

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

Льюис Келси
источник