内联名称空间有什么用?


334

C ++ 11允许inline namespaces,其所有成员也自动包含在其中namespace。我想不出这有什么用处-有人可以举一个简短,简洁的例子来说明这种情况inline namespace是需要的,也是最惯用的解决方案吗?

(另外,当发生了什么并不清楚,我namespace声明inline在一个但不是所有的声明,这可能住在不同的文件。这难道不是找麻烦?)

Answers:


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

根据的值__cplusplusvector选择一个或另一个实现。如果您的代码库是在C ++ 98之前的版本中编写的,并且您发现C ++ 98版本vector在升级编译器时给您造成麻烦,那么您要做的“全部”就是在其中找到对它的引用std::vector。您的代码库,并用替换它们std::pre_cxx_1997::vector

来的下一个标准,且STL供应商只需再次重复该过程,引入一个新的命名空间std::vectoremplace_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声明是在namespace中声明的std,因此用户可以正确地期望对其进行专门化。

该代码与非版本化的名称空间std或C ++ 11内联名称空间功能一起使用,但不适用于所使用的版本控制技巧using namespace <nested>,因为该代码公开了实现细节,即在其中vector定义的真实名称空间并非std直接存在的。

还有其他漏洞可用来检测嵌套的名称空间(请参阅下面的注释),但是内联名称空间将它们全部塞住。这就是全部。对于将来非常有用,但是AFAIK标准没有为自己的标准库规定内联名称空间名称(不过,我很乐意证明这是错误的),因此它只能用于第三方库,不能用于标准本身(除非编译器供应商同意命名方案)。


23
+1用于解释为何using namespace V99;在Stroustrup的示例中不起作用。
史蒂夫·杰索普

3
同样,如果我从头开始全新的C ++ 21实现,那么我也不想在中实现许多老废话std::cxx_11。并非每一个编译器都会始终实现标准库的所有旧版本,尽管目前很容易想到,添加新版本时将现有实现留在旧版本中的负担很小。无论如何。我想该标准可以做的有用的事情是使其成为可选的,但是如果存在,则带有一个标准名称。
史蒂夫·杰索普

46
这还不是全部。ADL也是一个原因(ADL不会使用指令来遵循),并且名称查找也是如此。(using namespace A如果要查找,在名称空间B中,名称空间B中的名称将隐藏在名称空间A中的名称B::name-内联名称空间则不是如此)。
Johannes Schaub-litb 2012年

4
为什么不只将ifdefs用于完整的矢量实现?所有实现都将在一个命名空间中,但是在预处理之后将仅定义其中之一
sasha.sochka 2013年

6
@ sasha.sochka,因为在这种情况下您不能使用其他实现。它们将被预处理器删除。使用内联名称空间,您可以通过指定完全限定的名称(或using关键字)来使用所需的任何实现。
Vasily Biryukov

70

http://www.stroustrup.com/C++11FAQ.html#inline-namespace(由Bjarne Stroustrup撰写并维护的文档,您认为应该了解大多数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,但是为了使Bjarne的话成为委员会的动机,我不必完全理解用例。


因此,实际上f(1)可以从内联V99名称空间调用最后一个版本吗?
Eitan T 2012年

1
@EitanT:是的,因为全局名称空间具有using namespace Mine;,并且Mine名称空间包含内联名称空间中的所有内容Mine::V99
史蒂夫·杰索普

2
@Walter:您在包含的发行版中inline从文件V99.h中删除V100.hMine.h当然,您同时也要进行修改以添加额外的包含。Mine.h是库的一部分,而不是客户端代码的一部分。
史蒂夫·杰索普

5
@walter:他们没有安装V100.h,他们正在安装一个名为“ Mine”的库。还有在“地雷”的99版3个文件- Mine.hV98.hV99.h。还有在“地雷”的100版本4个文件- ,,Mine.h 和。头文件的排列是与用户无关的实现细节。如果他们发现一些兼容性问题,这意味着他们需要专门使用部分或全部代码,则可以将对旧代码的调用与对新编写的代码的调用混合使用。V98.hV99.hV100.hMine::V98::fMine::V98::fMine::f
史蒂夫·杰索普

2
@Walter正如另一个答案所提到的,模板需要在声明它们的名称空间中专门化,而不是使用声明它们的名称空间。Mine,而不必专门研究Mine::V99Mine::V98
贾斯汀时间-恢复莫妮卡

8

除了所有其他答案。

内联名称空间可用于编码符号中的ABI信息或功能的版本。由于这个原因,它们被用来提供向后的ABI兼容性。内联名称空间使您可以将信息注入到整齐的名称(ABI)中,而无需更改API,因为它们仅影响链接器符号名称。

考虑以下示例:

假设您编写了一个函数Foo,该函数引用对象的say语句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构建一个对象,而使用debug标志构建另一个对象。您会发现链接器符号也包括内联名称空间名称。在这种情况下

$ nm rel.o
T__ ZROKfoo9relEbar
$ nm dbg.o
T__ ZROKfoo9dbgEbar

链接器符号名称可能不同。

注意符号名称中存在reldbg

现在,如果您尝试将调试与发布模式链接,反之亦然,则将获得一个链接器错误,而不是运行时错误。


1
是的,那很有道理。因此,对于库实现者等而言,更多。
Walter

3

实际上,我发现了内联名称空间的另一种用法。

借助Qt,您可以使用来获得一些额外的好功能Q_ENUM_NS,这又要求封闭的名称空间具有一个元对象,该对象用声明Q_NAMESPACE。但是,为了Q_ENUM_NS工作,Q_NAMESPACE 在同一文件file中必须有一个对应的文件。而且只能有一个,否则会出现重复的定义错误。有效地,这意味着所有枚举都必须在同一标头中。uck

或者...您可以使用内联名称空间。将枚举隐藏在an中inline namespace会导致元对象具有不同的名称,而像其他名称空间这样的用户查找对象并不存在。

因此,如果出于某种原因需要这样做,它们对于将内容拆分为多个看起来都像一个名称空间的子名称空间很有用。当然,这类似于using namespace inner在外部名称空间中进行写入,但是不会两次写入内部名称空间的名称而导致DRY违反。


  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引入了固定用的问题之前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行),并且使意图清晰明了。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.