C++编程语言:基础设施:命名空间(Bjarne Stroustrup)

第 14 章  命名空间(Namespaces)

目录

14.1  组成问题(Composition Problems)

14.2  命名空间(Namespaces)

14.2.1  显式修饰(Explicit Qualification)

14.2.2  使用using 声明

14.2.3  using 指令

14.2.4  参数依赖查询

14.2.5  命名空间的开放性

14.3  模块化和接口

14.3.1  显式修饰(Explicit Qualification)

14.3.2  实现

14.3.3  接口和实现

14.4  使用命名空间的组成

14.4.1  便捷性对比于安全性

14.4.2  命名空间别名

14.4.3  命名空间组成

14.4.4  组成和选择

14.4.5  命名空间和重载

14.4.6  版本控制 (Versioning)

14.4.7  嵌入命名空间 (Nested Namespaces)

14.4.8  无名命名空间 (Unnamed Namespaces)

14.4.9  C头文件 (C Headers)

14.5  建议


14.1  组成问题(Composition Problems)

         任何实际程序都由许多独立的部分组成。函数(§2.2.1,第 12 章)和类(§3.2,第 16 章)提供了相对细粒度的关注点分离,而“库”,源文件和编译单元(§2.4,第 15 章)提供了较粗粒度的关注点分离。逻辑上的理想是模块化,即保持独立事物的分离,并仅允许通过明确指定的接口访问“模块”。C++ 不提供支持模块概念的单一语言特性;没有模块构造。相反,模块化是通过其他语言功能(如函数、类和命名空间)和源代码组织的组合来表达的。

    本章和下一章将讨论程序的粗略结构及其作为源文件的物理表示。也就是说,这两章更关注整体编程,而不是单个类型、算法和数据结构的优雅表达。

    考虑一下当人们未能进行模块化设计时可能出现的一些问题。例如,图形库可以提供不同类型的图形形状和功能来帮助使用它们:

// Graph_lib:

class Shape { /* ... */ };

class Line : public Shape { /* ... */ };

class Poly_line: public Shape { /* ... */ }; // connected sequence of lines

class Text : public Shape { /* ... */ }; // text label

Shape operator+(const Shape&, const Shape&); // compose

Graph_reader open(const char); //open file of Shapes

现在有人提出了另一个库,提供文本操作功能:

// Te xt_lib:

class Glyph { /* ... */ };

class Word { /* ... */ }; // 字形(Glyphs)

class Line { /* ... */ }; // sequence of Words

class Text { /* ... */ }; // sequence of Lines

File open(const char); //open text file

Word operator+(const Line&, const Line&); // 连接

    暂时我们先忽略图形和文本处理的具体设计问题,只考虑在程序中同时使用Graph_libText_lib的问题。

    假设(足够现实)Graph_lib 的功能在标头(§2.4.1)Graph_lib.h 中定义,而 Text_lib 的功能在另一个标头 Text_lib.h 中定义。现在,我可以“无辜地”#include 两者并尝试使用这两个库中的功能:

#include "Graph_lib.h"

#include "Text_lib.h"

// ...

#include 那些头文件就会导致一连串的错误消息:LineTextopen() 被定义两次,编译器无法消除歧义。尝试使用这些库会给出更多错误消息。

有许多技术可以解决此类名称冲突。例如,可以通过将库的所有功能放在几个类中、使用所谓的不常见的名称(例如,Text_box 而不是 Text)或系统地使用库中名称的前缀(例如,gl_shape gl_line)来解决某些此类问题。这些技术(也称为“变通方法”和“黑客”)在某些情况下都有效,但它们并不通用,使用起来可能不方便。例如,名称往往会变得很长,而使用许多不同的名称会阻碍通用编程(§3.4)。

14.2  命名空间(Namespaces)

    命名空间的概念用于直接表示一组直接属于一起的设施的概念,例如库的代码。命名空间的成员处于同一作用域内,可以相互引用而无需特殊符号,而从命名空间外部访问则需要显式符号。特别是,我们可以通过将声明集(例如,库接口)分离到命名空间中来避免名称冲突。例如,我们可以将图库称为Graph_lib

namespace Graph_lib {

class Shape { /* ... */ };

class Line : public Shape { /* ... */ };

class Poly_line: public Shape { /* ... */ }; // connected sequence of lines

class Text : public Shape { /* ... */ }; // text label

Shape operator+(const Shape&, const Shape&); // compose

Graph_reader open(const char); //open file of Shapes

}

类似地,我们的文本库的明显名称是 Text_lib

namespace Text_lib {

class Glyph { /* ... */ };

class Word { /* ... */ }; // sequence of Glyphs

class Line { /* ... */ }; // sequence of Words

class Text { /* ... */ }; // sequence of Lines

File open(const char); //open text file

Word operator+(const Line&, const Line&); // concatenate

}

只要我们设法选择不同的命名空间名称,例如 Graph_libText_lib(§14.4.2),我们现在就可以将两组声明一起编译而不会发生名称冲突。

    命名空间应该表达某种逻辑结构:命名空间内的声明应该共同提供功能,在用户眼中将它们统一起来,并反映一组共同的设计决策。它们应该被视为一个逻辑单元,例如“图形库”或“文本操作库”,类似于我们考虑类的成员的方式。事实上,在命名空间中声明的实体被称为命名空间的成员。

    命名空间是一个(命名的)范围。您可以从后面的声明中访问命名空间中先前定义的成员,但您不能(不经过特别的努力)引用命名空间之外的成员。例如:

class Glyph { /* ... */ };

class Line { /* ... */ };

namespace Text_lib {

class Glyph { /* ... */ };

class Word { /* ... */ }; // sequence of Glyphs

class Line { /* ... */ }; // sequence of Words

class Text { /* ... */ }; // sequence of Lines

File open(const char); //open text file

Word operator+(const Line&, const Line&); // concatenate

}

Glyph glyph(Line& ln, int i); // ln[i]

    这里,Text_lib::operator+() 声明中的 WordLine 引用 Text_lib::Word Text_lib::Line。该局部名称查找不受全局 Line 的影响。相反,全局 glyph() 声明中的 Glyph Line 引用全局 ::Glyph ::Line。该(非局部)查找不受 Text_libGlyphLine 的影响。

    要引用命名空间的成员,我们可以使用它的完全限定名称。例如,如果我们想要一个使用 Text_lib 定义的 glyph(),我们可以这样写:

    Text_lib::Glyph glyph(Text_lib::Line& ln, int i); // ln[i]

从命名空间外部引用成员的其他方式有使用声明(§14.2.2)、使用指令(§14.2.3)和依赖参数的查找(§14.2.4)。

14.2.1  显式修饰(Explicit Qualification)

      可以在命名空间定义中声明成员,然后使用命名空间名称::成员名称表示法进行定义。

    必须使用以下符号来引入命名空间的成员:

namespace namespacename {

// declaration and definitions

}

例如:

namespace Parser {

double expr(bool); // declaration

double term(bool);

double prim(bool);

}

double val = Parser::expr(); // use

double Parser::expr(bool b) // definition

{

// ...

}

我们不能使用限定符语法(§iso.7.3.1.2)在命名空间定义之外声明命名空间的新成员。这样做的目的是为了捕获拼写错误和类型不匹配等错误,并且也使得在命名空间声明中找到所有名称变得相当容易。例如:

void Parser::logical(bool); // 错 : Parser 中没有logical()

double Parser::trem(bool); // 错: Parser 中无trem()(误拼)

double Parser::prim(int); // 错: Parser::prim()取bool参数(错误类型)

命名空间是一个范围。通常的作用域规则都适用于命名空间。因此,“命名空间”是一个非常基本且相对简单的概念。程序越大,命名空间就越有用,可以表达其各部分的逻辑分离。全局范围是一个命名空间,可以使用 :: 明确引用。例如:

int f(); // global function

int g()

{

int f; // 局变量; 隐藏了全局函数

f(); // 错: 不能调用一个int 变量

::f(); // OK: 调用全局函数

}

类是命名空间(§16.2)。

14.2.2  使用using 声明

    当名称在其命名空间之外频繁使用时,用其命名空间名称反复限定它可能会很麻烦。请考虑:

#include<string>

#include<vector>

#include<sstream>

std::vector<std::string> split(const std::string& s)

// 拆分成空格分隔的子字符串

{

std::vector<std::string> res;

std::istringstream iss(s);

for (std::string buf; iss>>buf;)

res.push_back(buf);

return res;

}

重复限定 std 是乏味且令人分心的。具体来说,我们在这个小例子中重复了四次 std::string。为了缓解这种情况,我们可以使用 using 声明来说明此代码中的 string 表示 std::string

using std::string; // 使用“string”就意味着“std::string”

std::vector<string> split(const string& s)

// 拆分成空格分隔的子字符串

{

std::vector<string> res;

std::istringstream iss(s);

for (string buf; iss>>buf;)

res.push_back(buf);

return res;

}

使用using声明将同义词引入作用域。通常,最好将局部同义词尽可能地保留在局部,以避免混淆。

    当用于重载名称时,using 声明适用于所有重载版本。例如:

namespace N {

void f(int);

void f(string);

};

void g()

{

using N::f;

f(789); //N::f(int)

f("Bruce"); // N::f(string)

}

有关在类层次结构中使用 using声明的信息,请参阅§20.3.5。

14.2.3  using 指令

    在 split() 示例(§14.2.2)中,在引入 std::string 的同义词后,我们仍然有三种 std:: 用法。通常,我们喜欢无限制地使用命名空间中的每个名称。这可以通过为命名空间中的每个名称提供 using 声明来实现,但这很繁琐,并且每次在命名空间中添加或删除新名称时都需要额外的工作。或者,我们可以使用 using 指令来请求在我们的作用域内无限制地访问命名空间中的每个名称。例如:

using namespace std; // 使得来自std的每一个名称都可访问

vector<string> split(const string& s)

// 拆分成空格分隔的子字符串

{

vector<string> res;

istringstream iss(s);

for (string buf; iss>>buf;)

res.push_back(buf);

return res;

}

using指令使命名空间中的名称可用,就好像它们是在其命名空间之外声明的一样(另请参阅 §14.4)。使用using指令使经常使用且众所周知的库中的名称无条件可用是一种流行的简化代码的技术。这是本书中用于访问标准库设施的技术。标准库设施在命名空间 std 中定义。

在函数中,可以安全地使用 using 指令来方便使用,但应谨慎使用全局 using 指令,因为过度使用可能会导致名称冲突,而命名空间正是为避免这种情况而引入的。例如:

namespace Graph_lib {

class Shape { /* ... */ };

class Line : Shape { /* ... */ };

class Poly_line: Shape { /* ... */ }; // connected sequence of lines

class Text : Shape { /* ... */ }; // text label

Shape operator+(const Shape&, const Shape&); // compose

Graph_reader open(const char); // open file of Shapes

}

namespace Text_lib {

class Glyph { /* ... */ };

class Word { /* ... */ }; // sequence of Glyphs

class Line { /* ... */ }; // sequence of Words

class Text { /* ... */ }; // sequence of Lines

File open(const char); // open text file

Word operator+(const Line&, const Line&); // concatenate

}

using namespace Graph_lib;

using namespace Text_lib;

Glyph gl; // Te xt_lib::Glyph

vector<Shape> vs; // Graph_lib::Shape

到目前为止,一切顺利。特别是,我们可以使用不冲突的名称,例如 GlyphShape。然而,现在只要我们使用其中一个冲突的名称,就会发生名称冲突 - 就像我们没有使用命名空间一样。例如:

Text txt; // 错 : 歧义

File fp = open("my_precious_data"); // 错 : 歧义

因此,我们必须小心使用全局范围内的 using 指令。特别是,除非在非常特殊的情况下(例如,为了帮助转换),否则不要将全局范围内的 using 指令放在头文件中,因为您永远不知道头文件可能在哪里被 #included

14.2.4  参数依赖查询

采用用户定义类型 X 的参数的函数通常与 X 定义在同一个命名空间中。因此,如果在函数的使用上下文中找不到该函数,我们将在其参数的命名空间中查找。例如:

namespace Chrono {

class Date { /* ... */ };

bool operator==(const Date&, const std::string&);

std::string format(const Date&); // 用字符串呈现

// ...

}

void f(Chrono::Date d, int i)

{

std::string s = format(d); // Chrono::format()

std::string t = format(i); // error : no format() in scope

}

与使用显式限定相比,此查找规则(称为参数依赖查找(argument-dependent lookup)或简称 ADL)为程序员节省了大量的输入工作,但它不会像 using 指令(§14.2.3)那样污染命名空间。它对于运算符操作数(§18.2.5)和模板参数(§26.3.5)尤其有用,因为显式限定可能非常麻烦。

    请注意,命名空间本身需要在作用域内,并且必须先声明该函数才能找到和使用它。

当然,一个函数可以从多个命名空间中获取参数类型。例如:

void f(Chrono::Date d, std::string s)

{

if (d == s) {

// ...

}

else if (d == "August 4, 1914") {

// ...

}

}

在这种情况下,我们会在调用范围(一如既往)和每个参数的命名空间(包括每个参数的类和基类)中查找该函数,并对找到的所有函数执行通常的重载解析(§12.3)。特别是,对于调用 d==s,我们会在 f() 周围的范围、std 命名空间(其中 == 定义为字符串)和 Chrono 命名空间中查找 operator==。有一个 std::operator==(),但它不接受 Date 参数,因此我们使用接受 Date 参数的 Chrono::operator==()。另请参阅 §18.2.5。

当类成员调用命名函数时,同一类及其基类的其他成员优先于根据参数类型可能找到的函数(运算符遵循不同的规则;§18.2.1、§18.2.5)。例如:

namespace N {

struct S { int i };

void f(S);

void g(S);

void h(int);

}

struct Base {

void f(N::S);

};

struct D : Base {

void mf();

void g(N::S x)

{

f(x); // call Base::f()

mf(x); // call D::mf()

h(1); // 错 : 没找到h(int)函数

}

};

在标准中,参数依赖查找的规则以关联命名空间的形式表述(§iso.3.4.2)。基本上:

• 如果参数是类成员,则关联的命名空间是类本身(包括其基类)和类的封闭命名空间。

• 如果参数是命名空间的成员,则关联的命名空间是封闭的命名空间。

• 如果参数是内置类型,则没有关联的命名空间。

      参数依赖查找可以节省大量繁琐且令人分心的输入,但偶尔也会产生令人惊讶的结果。例如,搜索函数 f() 的声明时,不会优先搜索调用 f() 的命名空间中的函数(而搜索调用 f() 的类中的函数时则不会优先搜索):

namespace N {

template<class T>

void f(T, int); // N::f()

class X { };

}

namespace N2 {

N::X x;

void f(N::X, unsigned);

void g()

{

f(x,1); // calls N::f(X,int)

}

}

选择 N2::f() 似乎很明显,但事实并非如此。应用重载解析并找到最佳匹配:N::f() f(x,1) 的最佳匹配,因为 1 int 而不是无符号数。相反,已经看到一些示例,其中选择了调用方命名空间中的函数,但程序员希望使用已知命名空间中的更好函数(例如,来自 std 的标准库函数)。这可能最令人困惑。另请参阅 §26.3.6。

14.2.5  命名空间的开放性

    命名空间是开放的也就是说,您可以从多个单独的命名空间声明中向其添加名称。例如:

namespace A {

int f(); // now A has member f()

}

namespace A {

int g(); // now A has two members, f() and g()

}

这样,命名空间的成员就不必连续地放在同一个文件中。当将旧程序转换为使用命名空间时,这一点很重要。例如,考虑一个不使用命名空间编写的头文件:

// my header :

void mf(); // my function

void yf(); // your function

int mg(); // my function

// ...

这里,我们(不明智地)只是添加了所需的声明,而没有考虑模块化。可以重写此代码而无需重新排序声明:

// my header :

namespace Mine {

void mf(); // my function

// ...

}

void yf(); // your function (未放入命名空间)

namespace Mine {

int mg(); // my function

// ...

}

在编写新代码时,我更喜欢使用许多较小的命名空间(参见 §14.4),而不是将真正重要的代码片段放入单个命名空间中。然而,当将软件的主要部分转换为使用命名空间时,这通常是不切实际的。

    在几个单独的命名空间声明中定义命名空间成员的另一个原因是,有时我们想要区分用作接口的命名空间部分和用于支持轻松实现的部分;§14.3 提供了一个示例。

    命名空间别名(§14.4.2)不能用于重新开放命名空间

14.3  模块化和接口

    任何现实程序都由多个独立部分组成。例如,即使是简单的“Hello, world!”程序也至少涉及两个部分:用户代码请求打印 Hello, world!,然后 I/O 系统执行打印。

    考虑第 10.2 节中的桌面计算器示例。它可以看作由五部分组成:

[1] 解析器,进行语法分析:expr()、term() prim();

[2] 词法分析器,用字符组成标记:Kind、Token、Token_stream ts;

[3] 符号表,保存 (string,value) 对:table ;

[4] 驱动程序main() calculate();

[5] 错误处理程序error() number_of_errors

可以用关系图表示为:

其中箭头表示“使用”。为了简化图示,我没有表示每个部分都依赖于错误处理。事实上,计算器被设想为三个部分,为了完整性,添加了驱动程序和错误处理程序。

当一个模块使用另一个模块时,它不需要了解所用模块的所有信息。理想情况下,模块的大多数细节对其用户来说都是未知的。因此,我们区分了模块和其接口。例如,解析器直接依赖于词法分析器的接口(仅),而不是完整的词法分析器。词法分析器仅实现其接口中宣传的服务。这可以像这样的关系图呈现:

虚线表示“实现”。我认为这是程序的真正结构,我们作为程序员的工作就是在代码中忠实地表示它。这样一来,代码就会变得简单、高效、易懂、可维护等,因为它将直接反映我们的基本设计。

以下小节展示了如何使桌面计算器程序的逻辑结构清晰,而 §15.3 展示了如何物理地组织程序源文本以利用它。计算器是一个很小的程序,所以在“现实生活中”,我不会像在这里一样费心使用命名空间和单独编译(§2.4.1,§15.1)。使计算器的结构明确只是对大型程序有用的技术的一种说明,而不会淹没在代码中。在实际程序中,由单独的命名空间表示的每个“模块”通常都有数百个函数、类、模板等。

错误处理贯穿于程序的结构。将程序分解为模块或(相反)用模块编写程序时,我们必须注意尽量减少由错误处理引起的模块间依赖关系。C++ 提供了异常,以将错误的检测和报告与错误处理分离开来(第 13 章第 2.4.3.1 节)。

除了本章和下一章讨论的概念之外,模块化的概念还有很多。例如,我们可以使用并发执行和通信任务(§5.3,第 41 章)或进程来表示模块化的重要方面。同样,使用单独的地址空间和在地址空间之间通信信息也是这里未讨论的重要主题。我认为这些模块化概念在很大程度上是独立和正交的。有趣的是,在每种情况下,将系统分成模块都很容易。难题是提供跨模块边界的安全、方便和高效的通信。

14.3.1  显式修饰(Explicit Qualification)

    命名空间是一种表达逻辑分组的机制。也就是说,如果一些声明根据某些标准在逻辑上属于一起,则可以将它们放在一个公共命名空间中以表达这一事实。因此,我们可以使用命名空间来表达计算器的逻辑结构。例如,来自台式计算器(§10.2.1)的解析器的声明可以放在命名空间Parser中:

namespace Parser {

double expr(bool);

double prim(bool get) { /* ... */ }

double term(bool get) { /* ... */ }

double expr(bool get) { /* ... */ }

}

必须首先声明函数 expr(),然后定义它来打破 §10.2.1 中描述的依赖循环。

    桌面计算器的输入部分也可以放在它自己的命名空间中:

namespace Lexer {

enum class Kind : char { /* ... */ };

class Token { /* ... */ };

class Token_stream { /* ... */ };

Token_stream ts;

}

符号表非常简单:

namespace Table {

map<string,double> table;

}

驱动程序不能完全放入命名空间,因为语言规则要求main()是一个全局函数:

namespace Driver {

void calculate() { /* ... */ }

}

int main() { /* ... */ }

错误处理程序也很简单:

namespace Error {

int no_of_errors;

double error(const string& s) { /* ... */ }

}

这种命名空间的使用明确了词法分析器和解析器向用户提供的内容。如果我包含了函数的源代码,这个结构就会被掩盖。如果函数体包含在实际大小的命名空间的声明中,您通常必须浏览大量信息才能找到所提供的服务,即找到接口。

    依赖单独指定接口的替代方法是提供一种工具,从包含实现细节的模块中提取接口。我认为这不是一个好的解决方案。指定接口是一项基本的设计活动,模块可以为不同的用户提供不同的接口,并且通常在实现细节具体化之前很久就设计好了接口。

    以下是接口与实现分离的Parser版本:

namespace Parser {

double prim(bool);

double term(bool);

double expr(bool);

}

double Parser::prim(bool get) { /* ... */ }

double Parser::term(bool get) { /* ... */ }

double Parser::expr(bool get) { /* ... */ }

请注意,由于将实现与接口分离,每个函数现在只有一个声明和一个定义。用户将只看到包含声明的接口。实现(在本例中为函数体)将放置在用户不需要查看的“别的某处”。

    在理想情况下,程序中的每个实体都属于某个可识别的逻辑单元(“模块”)。因此,非平凡程序中的每个声明理想情况下都应位于某个命名空间中,以指示其在程序中的逻辑角色。main() 是个例外,它必须是全局的,以便编译器将其识别为特殊函数(§2.2.1、§15.4)。

14.3.2  实现

    代码模块化后会是什么样子?这取决于我们如何决定访问其他命名空间中的代码。我们总是可以从“我们自己的”命名空间访问名称,就像我们在引入命名空间之前所做的一样。但是,对于其他命名空间中的名称,我们必须在显式修饰、使用声明和使用指令之间进行选择。

    Parser::prim() 为实现中命名空间的使用提供了一个很好的测试用例,因为它使用了其他每个命名空间(Driver 除外)。如果我们使用显式修饰,我们会得到:

double Parser::prim(bool get) // handle primaries

{

if (get) Lexer::ts.g et();

switch (Lexer::ts.current().kind) {

case Lexer::Kind::number: // floating-point constant

{

double v = Lexer::ts.current().number_value;

Lexer::ts.g et();

return v;

}

case Lexer::Kind::name:

{

double& v = Table::table[Lexer::ts.current().string_value];

if (Lexer::ts.g et().kind == Lexer::Kind::assign) v = expr(true); // ’=’ seen: assignment

return v;

}

case Lexer::Kind::minus: // unar y minus

return prim(true);

case Lexer::Kind::lp:

{

double e = expr(true);

if (Lexer::ts.current().kind != Lexer::Kind::rp) return Error::error(" ')' expected");

Lexer::ts.g et(); // eat ’)’

return e;

}

default:

return Error::error("primar y expected");

}

}

我数了一下 Lexer:: 出现了 14 次,并且(尽管有理论认为相反)我不认为更明确地使用模块化会提高可读性。我没有使用 Parser::,因为这在命名空间 Parser 中是多余的。

如果我们使用 using 声明,我们会得到:

using Lexer::ts; // 省掉‘‘Lexer::’’的8次出现

using Lexer::Kind; // 省掉‘‘Lexer::’’的6次出现

using Error::error; // 省掉‘‘Error ::’’的2次出现

using Table::table; // 省掉‘‘Table::’’的1次出现

double prim(bool get) // handle primaries

{

if (get) ts.get();

switch (ts.current().kind) {

case Kind::number: // floating-point constant

{

double v = ts.current().number_value;

ts.get();

return v;

}

case Kind::name:

{

double& v = table[ts.current().string_value];

if (ts.get().kind == Kind::assign) v = expr(true); // ’=’ seen: assignment

return v;

}

case Kind::minus: // unar y minus

return prim(true);

case Kind::lp:

{

double e = expr(true);

if (ts.current().kind != Kind::rp) return error("')' expected");

ts.get(); // eat ’)’

return e;

}

default:

return error("primar y expected");

}

}

我的猜测是 Lexer:: 的使用声明是值得的,但其他的价值则微不足道。

    如果我们使用 using 指令,我们会得到:

using namespace Lexer; // 省掉‘‘Lexer::’’的14次出现

using namespace Error; // 省掉‘‘Error ::’’的2次出现

using namespace Table; // 省掉‘‘Table::’’的1次出现

double prim(bool get) // handle primaries

{

// 如前

}

Error Table 的使用声明在符号上没有多大意义,并且可以说它们掩盖了以前修饰名称的起源。

    因此,必须根据具体情况在显式修饰、using 声明和 using 指令之间进行权衡。经验法则如下:

[1] 如果某些修饰对于多个名称来说确实很常见,请对该命名空间使用 using 指令。

[2] 如果某些限定对于命名空间中的特定名称很常见,请对该名称使用 using 声明。

[3] 如果名称的修饰不常见,请使用显式修饰来明确说明名称的来源。

[4] 不要对与用户位于同一命名空间中的名称使用显式修饰。

14.3.3  接口和实现

    应该清楚的是,我们为 Parser 使用的命名空间定义并不是 Parser 向其用户呈现的理想接口。相反,该 Parser 声明了一组声明,这些声明是方便编写单个Parser函数所需的。Parser 向其用户提供的接口应该简单得多:

namespace Parser { // 用户接口

double expr(bool);

}

我们看到命名空间 Parser 用于提供两件事:

    [1] 实现解析器的函数的公共环境。

[2] 解析器向其用户提供的外部接口。

因此,驱动程序代码 main() 应该只看到用户接口。

    实现解析器的函数应该将我们决定的接口视为表达这些函数共享环境的最佳接口。即:

namespace Parser { // 实现接口

double prim(bool);

double term(bool);

double expr(bool);

using namespace Lexer; // 使用lexer接供的所有设施

using Error::error;

using Table::table;

}

或者图形化:

箭头表示“依赖于所提供的接口”关系。

    我们可以为用户的接口和实现者的接口赋予不同的名称,但(因为命名空间是开放的;§14.2.5)我们不必这样做。缺少单独的名称不一定会导致混淆,因为程序的物理布局(参见§15.3.2)自然提供了单独的(文件)名称。如果我们决定使用单独的实现命名空间,设计在用户看来不会有什么不同:

namespace Parser { // user interface

double expr(bool);

}

namespace Parser_impl { // 实现器接口

using namespace Parser;

double prim(bool);

double term(bool);

double expr(bool);

using namespace Lexer; //由Lexer所提供的所有设施

using Error::error;

using Table::table;

}

或图形化为:

 

对于较大的程序,我倾向于引入 _impl 接口。

    提供给实施者的接口比提供给用户的接口大。如果这个接口是用于实际系统中实际大小的模块,那么它会比用户看到的接口更频繁地发生变化。重要的是模块(在本例中为使用ParserDriver)的用户不受此类变化的影响。

14.4  使用命名空间的组成

    在较大的程序中,我们倾向于使用许多命名空间。本节探讨了使用命名空间编写代码的技术方面。

14.4.1  便捷性对比于安全性

    using 声明将名称添加到局部作用域using 指令则不然,它只是使名称在声明的范围内可访问。例如:

namespace X {

int i, j, k;

}

int k;

void f1()

{

int i = 0;

using namespace X; // 使来自于X 的名称可直接访问

i++; //local i

j++; //X::j

k++; //错: X的 k 还是全局的k?

::k++; //全局的k

X::k++; //X的k

}

void f2()

{

int i = 0;

using X::i; // 错: i 在f2()中声明两次(using在此是声明符)

using X::j;

using X::k; // 隐藏全局k

i++;

j++; //X::j

k++; //X::k

}

局部声明的名称(由普通声明或使用using声明)隐藏了同名的非局部声明,并且在声明点检测到名称的任何无效重载。

    请注意 f1() k++ 的歧义错误。全局名称不会优先于全局作用域内可访问的命名空间中的名称。这可以有效防止意外名称冲突,而且更重要的是,可以确保不会因污染全局命名空间而获得任何好处。

    当通过using指令可以访问声明许多名称的库时,未使用的名称冲突不会被视为错误,这是一个显著的优势。

14.4.2  命名空间别名

    如果用户为其命名空间指定短名称,则不同命名空间的名称将发生冲突:

namespace A {// 知名, 将冲突 (事实上)

// ...

}

A::String s1 = "Grieg";

A::String s2 = "Nielsen";

然而,长命名空间名称在实际代码中可能不切实际:

namespace American_Telephone_and_Telegraph { // 太长

// ...

}

American_Telephone_and_Telegraph::String s3 = "Grieg";

American_Telephone_and_Telegraph::String s4 = "Nielsen";

可以通过为较长的命名空间名称提供短别名来解决此难题:

// use namespace alias to shorten names:

namespace ATT = American_Telephone_and_Telegraph;

ATT::String s3 = "Grieg";

ATT::String s4 = "Nielsen";

命名空间别名还允许用户引用“库”,并有一个声明来定义该库到底是什么。例如:

namespace Lib = Foundation_library_v2r11;

// ...

Lib::set s;

Lib::String s5 = "Sibelius";

这可以极大地简化用另一个版本替换库的任务。通过直接使用 Lib 而不是 Foundation_library_v2r11,您可以通过更改 Lib 别名的初始化并重新编译来更新到版本“v3r02”。重新编译将捕获源级不兼容性。在另一方面,过度使用别名(任何类型的别名)都会导致混乱。

14.4.3  命名空间组成

    我们经常想用现有的接口来组成一个接口。例如:

namespace His_string {

class String { /* ... */ };

String operator+(const String&, const String&);

String operator+(const String&, const char);

void fill(char);

// ...

}

namespace Her_vector {

template<class T>

class Vector { /* ... */ };

// ...

}

namespace My_lib {

using namespace His_string;

using namespace Her_vector;

void my_fct(String&);

}

鉴于此,我们现在可以根据 My_lib 编写程序:

void f()

{

My_lib::String s = "Byron"; // finds My_lib::His_string::Str ing

// ...

}

using namespace My_lib;

void g(Vector<String>& vs)

{

// ...

my_fct(vs[5]);

// ...

}

如果在提到的命名空间中没有声明显式修饰的名称(例如 My_lib::String),则编译器会在using指令中提到的命名空间中查找(例如 His_string)。

    只有当我们需要定义某些东西时,我们才需要知道实体的真实命名空间:

void My_lib::fill(char c) // 错:在My_lib中没有声明的fill()

{

// ...

}

void His_string::fill(char c) // OK: fill() declared in His_string

{

// ...

}

void My_lib::my_fct(String& v)// OK: String is My_lib::String, meaning His_string::Str ing

{

// ...

}

理想情况下,命名空间应该:

    [1] 表达一组逻辑上连贯的功能,

[2] 不让用户访问不相关的功能,

[3] 不会给用户带来过多的符号负担。

结合 #include 机制(§15.2.2),这里和以下小节中介绍的组合技术为此提供了强有力的支持。

14.4.4  组成和选择

    将组成(通过using指令)与选择(通过using声明)相结合,可实现大多数实际示例所需的灵活性。借助这些机制,我们可以提供对各种设施的访问,从而解决由组合引起的名称冲突和歧义。例如:

namespace His_lib {

class String { /* ... */ };

template<class T>

class Vector { /* ... */ };

// ...

}

namespace Her_lib {

template<class T>

class Vector { /* ... */ };

class String { /* ... */ };

// ...

}

namespace My_lib {

using namespace His_lib; //来自His_lib的一切

using namespace Her_lib; // 来自Her_lib的一切

using His_lib::String; // 解决潜在冲突,以利于 His_lib

using Her_lib::Vector; // 解决潜在冲突,以利于 Her_lib

template<class T>

class List { /* ... */ }; // 额外的stuff

// ...

}

当查看命名空间时,在那里显式声明的名称(包括使用using声明的名称)优先于using指令在另一个范围内可访问的名称(另请参阅 §14.4.1)。因此,My_lib 的用户将看到 StringVector 的名称冲突得到解决,有利于 His_lib::String Her_lib::Vector。此外,默认情况下将使用 My_lib::List,无论 His_lib Her_lib 是否提供 List

    通常,我倾向于在将名称包含到新命名空间时保持不变。这样,我就不必记住同一实体的两个不同名称。然而,有时需要使用新名称,或者只是希望有新名称。例如:

namespace Lib2 {

using namespace His_lib; // everything from His_lib

using namespace Her_lib; // everything from Her_lib

using His_lib::String; // resolve potential clash in favor of His_lib

using Her_lib::Vector; // resolve potential clash in favor of Her_lib

using Her_string = Her_lib::String; // rename

template<class T>

using His_vec = His_lib::Vector<T>; // rename

template<class T>

class List { /* ... */ }; // additional stuff

// ...

}

没有用于重命名的通用语言机制,但对于类型和模板,我们可以通过using(§3.4.5,§6.5)引入别名。

14.4.5  命名空间和重载

    函数重载(§12.3)可跨命名空间有效。这对于我们迁移现有库以使用命名空间并尽量减少源代码更改至关重要。例如:

// old A.h:

void f(int);

// ...

// old B.h:

void f(char);

// ...

// old user.c:

#include "A.h"

#include "B.h"

void g()

{

f('a'); // 调用B.h中的f()

}

该程序可以升级到使用命名空间的版本,而无需更改实际代码:

// new A.h:

namespace A {

void f(int);

// ...

}

// new B.h:

namespace B {

void f(char);

// ...

}

// new user.c:

#include "A.h"

#include "B.h"

using namespace A;

using namespace B;

void g()

{

f('a'); // 调用B.h中的 f()

}

如果我们想保持 user.c 完全不变,我们会将using 指令放在头文件中。但是,通常最好避免在头文件中使用 using 指令,因为将它们放在那里会大大增加名称冲突的可能性。

此重载规则还提供了一种扩展库的机制。例如,人们经常想知道为什么他们必须明确提及序列才能使用标准库算法来操作容器。例如:

sort(v.begin(),v.end());

为什么不写成:

    sort(v);

原因在于我们需要通用性(§32.2),但操作容器是迄今为止最常见的情况。我们可以像这样去适应这种情况:

#include<algorithm>

namespace Estd {

using namespace std;

template<class C>

void sort(C& c) { std::sort(c.begin(),c.end()); }

template<class C, class P>

void sort(C& c, P p) { std::sort(c.begin(),c.end(),p); }

}

Estd(我的“扩展 std”)提供了经常需要的 sort() 容器版本。这些当然是使用 <algorithm> 中的 std::sort() 实现的。我们可以像这样使用它:

using namespace Estd;

template<class T>

void print(const vector<T>& v)

{

for (auto& x : v)

cout << v << ' ';

cout << '\n';

}

void f()

{

std::vector<int> v {7, 3, 9, 4, 0, 1};

sort(v);

print(v);

sort(v,[](int x, int y) { return x>y; });

print(v);

sort(v.begin(),v.end());

print(v);

sort(v.begin(),v.end(),[](int x, int y) { return x>y; });

print(v);

}

命名空间查找规则和模板的重载规则确保我们找到并调用sort()的正确变体并获得预期的输出:

0 1 3 4 7 9

9 7 4 3 1 0

0 1 3 4 7 9

9 7 4 3 1 0

如果我们从 Estd 中删除 using namespace std;,此示例仍可行,因为 stdsort() 可通过参数相关查找找到(§14.2.4)。但是,我们无法找到在 std 之外定义的我们自己的容器的标准 sort()

14.4.6  版本控制 (Versioning)

    对于许多类型的接口来说,最艰难的考验是应对一系列新版本。考虑一个广泛使用的接口,例如 ISO C++ 标准头文件。一段时间后,定义了一个新版本,例如 C++98 头文件的 C++11 版本。可能添加了函数、重命名了类、删除了专有扩展(这些扩展本不应该存在)、更改了类型、修改了模板。为了让实现者的生活“有趣”,数亿行代码“在那里”使用旧头文件,而新版本的实现者永远无法看到或修改它们。不用说,破坏这样的代码会引起公愤,没有新的更好的版本也会引起公愤。到目前为止描述的命名空间设施可用于处理这个问题,只有非常小的例外,但是当涉及大量代码时,“非常小”仍然意味着大量的代码。因此,有一种在两个版本之间进行选择的方法,可以简单而明显地保证用户看到的只有一个特定版本。这称为内联命名空间(inline namespace):

namespace Popular {

inline namespace V3_2 {//V3_2 提供了Popular double f(double)的缺省含义;

int f(int);

template<class T>

class C { /* ... */ };

}

namespace V3_0 {

// ...

}

namespace V2_4_2 {

double f(double);

template<class T>

class C { /* ... */ };

}

}

这里,Popular 包含三个子命名空间,每个子命名空间定义一个版本。内联指定 V3_2 Popular 的默认含义。因此我们可以这样写:

using namespace Popular;

void f()

{

f(1); //Popular::V3_2::f(int)

V3_0::f(1); // Popular ::V3_0::f(double)

V2_4_2::f(1); // Popular ::V2_4_2::f(double)

}

template<class T>

Popular::C<T> { /* ... */ };

这种inline namespace解决方案具有侵入性;也就是说,要更改默认版本(子命名空间),需要修改标头源代码。此外,天真地使用这种处理版本的方式将涉及大量重复(不同版本中的通用代码)。但是,可以使用 #include 技巧将这种重复最小化。例如:

// file V3_common:

// ... 若干声明 ...

// file V3_2:

namespace V3_2 { // V3_2 提供了 Popular 和double f(double)和缺省含义;

int f(int);

template<class T>

class C { /* ... */ };

#include "V3_common"

}

// file V3_0.h:

namespace V3_0 {

#include "V3_common"

}

// file Popular.h:

namespace Popular {

inline

#include "V3_2.h"

#include "V3_0.h"

#include "V2_4_2.h"

}

除非真的有必要,否则我不建议如此复杂地使用头文件。上面的示例反复违反了禁止包含到非局部作用域以及禁止语法构造跨越文件边界(使用inline)的规则;请参阅§15.2.2。遗憾的是,我见过更糟糕的情况。

    在大多数情况下,我们可以通过侵入性较低的方式实现版本控制。我能想到的唯一一个完全不可能通过其他方式实现的例子是使用命名空间名称明确地对模板进行特化(例如,Popular::C<T>)。然而,在许多重要情况下,“在大多数情况下”还不够好。此外,基于其他技术组合的解决方案显然不太完全正确。

14.4.7  嵌入命名空间 (Nested Namespaces)

命名空间的一个明显用途是将一整套声明和定义包装在一个单独的命名空间中

namespace X {

// ... all my declarations ...

}

声明列表通常包含命名空间。因此,允许嵌套命名空间。这是出于实际原因,也出于构造应该嵌套的简单原因,除非有充分理由不嵌套。例如:

void h();

namespace X {

void g();

// ...

namespace Y {

void f();

void ff();

// ...

}

}

适用通常的作用域和修饰规则:

void X::Y::ff()

{

f(); g(); h();

}

void X::g()

{

f(); // 错:在X 中无 f()

Y::f(); // OK

}

void h()

{

f(); //错: 无全局 f()

Y::f(); // 错: 无全局Y

X::f(); // 错: 在X 中无 f()

X::Y::f(); // OK

}

有关标准库中嵌套命名空间的示例,请参阅 chrono (§35.2) 和 rel_ops(§35.5.3)。

14.4.8  无名命名空间 (Unnamed Namespaces)

    有时将一组声明包装在命名空间中很有用,只是为了防止名称冲突的可能性。也就是说,目的是保留代码的局部性,而不是向用户呈现接口。例如:

#include "header.h"

namespace Mine {

int a;

void f() { /* ... */ }

int g() { /* ... */ }

}

    由于我们不希望 Mine 这个名称在局部上下文之外为人所知,因此发明一个可能意外与其他人的名称冲突的冗余全局名称只会带来麻烦。在这种情况下,我们可以简单地让命名空间不带名称:

#include "header.h"

namespace {

int a;

void f() { /* ... */ }

int g() { /* ... */ }

}

显然,必须有某种方式从无名命名空间外部访问无名命名空间的成员。因此,无名命名空间具有隐含的using指令。前面的声明相当于:

namespace $$$ {

int a;

void f() { /* ... */ }

int g() { /* ... */ }

}

using namespace $$$;

    其中 $$$ 是定义命名空间的作用域所特有的某个名称。具体而言,不同编译单元中的无名命名空间是不同的。正如所期望的那样,无法从另一个编译单元命名无名命名空间的成员。

14.4.9  C头文件 (C Headers)

    考虑规范的第一个 C 程序:

#include <stdio.h>

int main()

{

printf("Hello, world!\n");

}

破坏这个程序不是一个好主意。将标准库作为特殊情况也不是一个好主意。因此,命名空间的语言规则旨在使将没有命名空间编写的程序转换为使用命名空间的更明确结构的程序变得相对容易。事实上,计算器程序(§10.2)就是一个例子。

    在命名空间中提供标准 C 的I/O 设施的一种方法是,将 C 头文件 stdio.h 中的声明放在命名空间 std 中:

// cstdio:

namespace std {

int printf(const char ... );

// ...

}

有了这个<cstdio>,我们可以通过添加 using 指令来提供向后兼容性:

// stdio.h:

#include<cstdio>

using namespace std;

这个 <stdio.h> 使 Hello, world! 程序编译成功。不幸的是,using 指令使得命名空间 std 中的每个名称都可以在全局命名空间中访问。例如:

#include<vector> // 注意避免污染全局命名空间

vector v1; // 错误: 在全局作用域无 ‘‘vector’’

#include<stdio.h> // 含有一个‘‘using namespace std;’’

vector v2; // oops: 现在这个有效

因此,标准要求 <stdio.h> 仅将来自 <cstdio> 的名称放置在全局范围内。这可以通过为 <cstdio> 中的每个声明提供一个 using 声明来实现:

// stdio.h:

#include<cstdio>

using std::printf;

// ...

另一个优点是 printf() 的使用声明可防止用户(意外或故意)在全局范围内定义非标准 printf()。我认为非局部using指令主要是一种过渡工具。我还将它们用于基本基础库,例如 ISO C++ 标准库 (std)。大多数引用其他命名空间中名称的代码都可以使用显式修饰和using声明更清楚地表达。

    命名空间和链接之间的关系在§15.2.5 中描述。

14.5  建议

[1] 使用命名空间表达逻辑结构;§14.3.1。

[2] 将除 main() 之外的每个非局部名称放在某个命名空间中;§14.3.1。

[3] 设计一个命名空间,以便您可以方便地使用它,而不会意外访问不相关的命名空间;§14.3.3。

[4] 避免使用非常短的命名空间名称;§14.4.2。

[5] 如有必要,使用命名空间别名缩写长命名空间名称;§14.4.2。

[6] 避免给命名空间的用户带来沉重的符号负担;§14.2.2、§14.2.3。

[7] 对接口和实现使用单独的命名空间;§14.3.3。

[8] 定义命名空间成员时使用 Namespace::member 符号;§14.4。

[9] 使用inline命名空间支持版本控制;§14.4.6。

[10] 使用 using 指令进行过渡、基础库(如 std)或在局部范围内使用;§14.4.9。

[11] 不要将 using 指令放在头文件中;§14.2.3。

内容来源:

<<The C++ Programming Language >> 第4版,作者 Bjarne Stroustrup

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1541619.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

接口加解密及数据加解密

目录 一、 加解密方式介绍 1.1 Hash算法加密 1.2. 对称加密 1.3 非对称加密 二、 我们要讲什么&#xff1f; 三、 接口加解密 四、 数据加解密 一、 加解密方式介绍 所有的加密方式我们可以分为三类&#xff1a;对称加密、非对称加密、Hash算法加密。 算法内部的具体实现…

自定义dialog 背景属性差异

比如正常要实现的dialog效果&#xff1a; 代码如此&#xff1a; public class SimpleDialog extends Dialog {private Button permissionokTv;//确定按钮private Button permissionnoTv;//取消按钮private TextView permissiontitleTv;//消息标题文本private TextView permiss…

[OpenGL]使用OpenGL绘制带纹理三角形

一、简介 本文介绍了如何使用使用OpenGL绘制带纹理三角形。 在绘制带纹理的三角形时&#xff0c; 首先使用.h读取准备好的.png格式的图片作为纹理&#xff0c;然后在fragment shader中使用 ... in vec2 textureCoord; uniform sampler2D aTexture1; void main() {FragColor …

Elionix 电子束曝光系统

Elionix 电子束曝光系统 - 上海纳腾仪器有限公司 -

您可能一直在寻找的 10 个非常有用的前端库

文章目录 前言正文1.radash2.dayjs3.driver4.formkit/drag-and-drop5.logicflow6.ProgressBar7.tesseract8.zxcvbn9.sunshine-track10.lottie 前言 前端开发中&#xff0c;总有一些重复性的工作让我们疲于奔命。为了提高开发效率&#xff0c;我们精心挑选了10个功能强大、易于…

数据结构与算法——Java实现 7.习题——反转链表

当你穿过了暴风雨&#xff0c;你已不是原来那个人 —— 24.9.21 206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输…

【Stm32】从零建立一个工程

这里我们创建“STM32F103”系列的文件&#xff0c;基于“固件库” 1.固件库获取 https://www.st.com.cn/zh/embedded-software/stm32-standard-peripheral-libraries.html 2.使用Keil创建.uvprojx文件 前提是已经下载好了“芯片对应的固件” 3.复制底层驱动代码 将固件库下的…

大数据新视界 --大数据大厂之JavaScript在大数据前端展示中的精彩应用

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

linux安装Anaconda3

先将Anaconda3安装包下载好&#xff0c;然后在主文件夹里新建一个文件夹&#xff0c;将Anaconda3安装包拖进去。 打开终端未来不出现缺东西的异常情况&#xff0c;我们先安装 yum install -bzip2然后进入根目录下&#xff0c;在进入Anaconda3文件夹下 sh包安装方式 sh Anac…

【二十四】【QT开发应用】ScorllArea应用3,补全ScorllArea代码以及ListWidget与ScorllArea联动的信号槽和槽函数编写

补全ScorllArea代码逻辑 我们将其他ListItem项目全部设置成和基本设置一样的代码&#xff0c;唯独不一样的就是把题头的label修改成对应的文本&#xff0c;例如基本设置&#xff0c;云盘设置等。 Widget对应一个类 每一个Widget创建对应的类&#xff0c;头文件和cpp文件&am…

为什么大多数的程序员的编程界面背景都是黑色的?

不光编程IDE软件界面是黑色&#xff0c;市场上很多软件也是黑色或灰色背景为主&#xff0c;比如PS、Pr、AutoCAD等。很多商业PPT、设计广告是黑色背景&#xff0c;这几年不少汽车品牌logo也改成单黑色。 看来黑色不光是程序员的偏爱&#xff0c;也是符合大多数人需求的颜色。 …

数字基带之相移键控PSK

1 相移键控定义 相移键控是指用载波的相移位变化来传递信号&#xff0c;不改变载波的幅度和频率&#xff0c;可用下面的公式表示。 是载波的幅度&#xff0c;是载波的角频率&#xff0c;是载波的瞬时相位&#xff0c;是载波的初始相位。如果需要调制的信号为1bit的二进制数&am…

链表(单向不带头非循环)

声明 链表题考的都是单向不带头非循环&#xff0c;所以在本专栏中只介绍这一种结构&#xff0c;实际中链表的结构非常多样&#xff0c;组合起来就有8种链表结构。 链表的实现 创建一个链表 注意&#xff1a;此处简单粗暴创建的链表只是为了初学者好上手。 public class MyS…

Spring(三)Spring事件+计划任务+条件注解+SpringAware

Application Event 事件 当一个Bean处理完一个任务之后&#xff0c;希望另一个Bean知道并做出相应的处理&#xff0c;这时需要让另外一个Bean监听当前Bean所发送的事件 自定义事件&#xff0c;集成ApplicationEvent自定义事件监听器&#xff0c;实现ApplicationListener使用容…

S-Clustr-Simple 飞机大战:骇入现实的建筑灯光游戏

项目地址:https://github.com/MartinxMax/S-Clustr/releases Video https://www.youtube.com/watch?vr3JIZY1olro 飞机大战 这是一个影子集群的游戏插件&#xff0c;可以将游戏画面映射到现实的设备&#xff0c;允许恶意控制来完成游戏。亦或者设备部署在某建筑物中,来控制…

电脑硬件-机械硬盘

简介 机械硬盘是电脑的主要存储媒介之一&#xff0c;通常用于存储一些文件资料或者学习视频笔记等比较大的内容。 结构 采用磁盘存储数据&#xff0c;使用温彻斯特的结构&#xff0c;特有四个特点&#xff1a; 1.磁头、盘片和运动机构安装在一个密封的腔体内。 2.盘片告诉旋…

AI大模型算法工程师经典面试题————为什么 Bert 的三个 Embedding 可以进行相加?

大模型算法工程师经典面试题————为什么 Bert 的三个 Embedding 可以进行相加&#xff1f; 为什么 Bert 的三个 Embedding 可以进行相加&#xff1f; Token Embedding、Segment Embedding、Position Embedding的意义我已经清楚了&#xff0c;但是这三个向量为什么可以相加…

数据中台系统产品原型RP原型Axure高保真交互原型 源文件分享

在数字化时代&#xff0c;数据已经成为企业最宝贵的资产之一。为了更好地管理和利用这些数据&#xff0c;这边为大家整理了一套数据中台Axure高保真原型。这套原型致力于为企业提供全方位的数据服务&#xff0c;助力企业实现数据驱动的创新发展。 下载及预览地址&#xff1a;h…

MATLAB智能优化算法-学习笔记(3)——大规模邻域搜索算法求解旅行商问题【过程+代码】

一、问题描述 旅行商问题(TSP, Traveling Salesman Problem)是组合优化中的经典问题之一。给定一组城市和每对城市之间的距离,要求找到一条最短的路径,使旅行商从某个城市出发,访问每个城市一次并最终回到出发点。TSP问题广泛应用于物流配送、工厂调度、芯片制造等领域。…

1、等保测评介绍

数据来源&#xff1a;等保测评基础知识学习(1.02.0)2024最新版_哔哩哔哩_bilibili 等级保护的定义&#xff1a; 对国家秘密信息、法人或其他组织及公民专有信息以及公开信息&#xff0c;按照其重要程度对信息系统实施分等级安全保护。这包括对使用的安全产品进行等级管理&…