C++ 关键字 :using

        

        

        在 C++ 庞大语法体系中, using 关键字十分的灵活多用,它可不简单。

        除了常规的引入命名空间之外,它还可用于引入枚举类型枚举器、定义常规类型别名、模板类型别名等。在定义常规类型别名方面与C语言中的typedef、#define与之相似,但又不尽相同。

        简单归纳起来, using 的主要作用是 “引入” “定义别名”。

        

引入

引入命名空间       

using namespace ns-name ;

        引入命名空间是最常见也最好理解的用法,如:

#include <iostream>
using namespace std;

int main() {
    cout << "Hello World!" << endl;
    // std::cout << "Hello World!" << endl;
    return 0;
}

         在这个示例中,我们使用 using namespace 将命名空间 std 的全部成员引入到当前命名空间(根空间)中。

        这样做的好处是,代码变得简洁,有效作用域内,都可以省略命名空间,直接访问。

        有效作用域是指从 using 声明点开始,直到包含该using声明的作用域结尾。

 using 关键字可以引入整个命名空间或者特定的命名空间成员。

        该怎样直观的理解这句话呢 ,看如下代码:

namespace
{
    int i; // 定义 ::(unique)::i
}
 
void f()
{
    i++;   //  定义 ::(unique)::i
}
 
namespace A
{
    namespace
    {
        int i;        // A::(unique)::i
        int j;        // A::(unique)::j
    }
 
    void g() { i++; } // A::(unique)::i++
}
 
using namespace A; //  将 A 中的所有名称引入全局名称空间
 
void h()
{
    i++;    // error: ::(unique)::i and ::A::(unique)::i 都在作用域中
    A::i++; // ok, 递增 ::A::(unique)::i
    j++;    // ok, 递增 ::A::(unique)::j
}

        由例子可见,因为引入了命名空间A,其下的所有成员都将暴露出来。变量 i  在两个命名空间中都有声明,因此产生了冲突。

        因此,在.h头文件中,一般不应该使用using声明,避免产生名字冲突。

        方便的同时,也带来了问题,如命名空间污染。        

#include <iostream>
#include <utility>
using namespace std;

void move(int &&x) {
    cout << "Move: " << x << endl;
}

int main() {
    move(10);  // 调用 void move(int &&x);
    std::move(10);  // 调用 std::move
    return 0;
}

        在这个示例中 move(10) 会调用本源文件的 move,也许这并不是你的预期。当然,我们依然可以使用 std::move 显式调用。

        💡 命名空间污染是十分隐晦和危险的,特别是当多个命名空间中有相同名称的成员时,需要特别的注意。

        另外,这里还有一个“后续扩展成员不可见的”规则,即在 using 后访问的命名空间,无法访问到后续扩展命名空间时新增的成员,有说起来点绕,看示例:        

namespace A
{
	int i;
}

int main()
{
	A::i++; // ok
	A::j++; // error
}

namespace A
{
	int j;
}

        i 在 using 之前就定义了,可以正常访问, j 在using、访问之后定义,不可见。

        由此规则,可能会衍生一个潜在的混淆: 

namespace A
{
	void f(int) {}; // 定义一个接受int参数的函数f
}
using A::f; // 使得全局命名空间中的::f成为A::f(int)的同义词

// 扩展命名空间A
namespace A
{
	void f(char) {}; // 定义一个接受char参数的函数f,不改变全局命名空间中::f的含义
}


int main()
{
	f('a');     // 调用 void f(int),参数隐式转换
	using A::f; // 重新引入完整的命名空间A
	f('a');     // 调用 void f(char) 
}

        一个例外是函数模板特化:如果引入的是类模板,后续添加的特化版本是可见的,因为它们的查找是通过主模板进行的。

引入命名空间的实体

       

using ns-name :: member-name ;

        相对于引入整个命名空间,引入命名空间内的实体操作更为细腻,如:

#include <iostream>
using std::cin;
using std::cout;
using std::endl;

int main() {
    cout << "Hello World!" << endl;
    // std::cout << "Hello World!" << endl;
    return 0;
}

        在这个示例中,我们只引入了 std命名空间中的cin、cout、endl 实体成员。当然这也仅仅是降低了命名空间污染的可能性,最终,需要我们自己审慎把握。

💡 从 C++ 17 开始,允许使用 分隔,同时引入多个命名空间成员。

using std::cin, std::cout, std::endl;

引入类的继承体系

        

using classscope::member;

       首先,在类的继承体系中,using 使得派生类显式地重新暴露基类的被隐藏或覆盖的成员函数,使得基类的成员函数在派生类的作用域内重新可用。


class Base {
public:
	virtual void foo() { /*...*/ }
	void bar() {}
};

class Derived : public Base {
public:
	void foo() override { /*...*/ } // 重写foo

	using Base::foo; // 显式再次引入Base的foo版本

	void bar() {}
	using Base::bar;
};

int main()
{
	Derived d;
	d.foo();		// 调用 Derived::foo();
	d.Base::foo();	// 调用被覆盖的	Base::foo();

	d.bar();		// 调用 Derived::bar();
	d.Base::bar();	// 调用被隐藏的 Base::bar();
}

        其次,在子类私有继承时,可以改变成员的可见性,如:

class Base {
public:
    void foo() {}
	void bar() {}
};

class Derived : private Base {
public:
	using Base::foo;    // 显式引入Base的foo
	//using Base::bar;  // 未引入Base的bar
};

int main()
{
	Derived d;
	d.foo();		// Base::foo() 可访问
	d.bar();		// Base::bar() 可访问
}

继承构造函数 (C++11)

派生类可以使用 using 声明从直接基类继承构造函数

        在C++11之前,如果基类有一个构造函数需要参数,那么在派生类中必须显式地调用这个构造函数,当基类的构造函数参数很多、版本很多的时候,这将是一件繁重、容易出错的工作。

        现在,使用 using 即可全部引入。

#include <iostream>
using namespace std;

class Base
{
public:
    Base() { cout << "Base()" << endl; }
    Base(const Base& other) { cout << "Base(Base&)" << endl; }
    explicit Base(int i) : num(i) { cout << "Base(int)" << endl; }
    explicit Base(char c) : letter(c) { cout << "Base(char)" << endl; }

private:
    int num;
    char letter;
};

class Derived : Base
{
public:
    // 继承基类所有构造函数
    using Base::Base;

private:
    // 不能从构造函数初始化成员,需要手动初始化
    int newMember{ 0 };
};

int main()
{
    cout << "Derived d1(5) calls: ";
    Derived d1(5);
    cout << "Derived d1('c') calls: ";
    Derived d2('c');
    cout << "Derived d3 = d2 calls: " ;
    Derived d3 = d2;
    cout << "Derived d4 calls: ";
    Derived d4;
}

观察输出

Derived d1(5) calls: Base(int)
Derived d1('c') calls: Base(char)
Derived d3 = d2 calls: Base(Base&)
Derived d4 calls: Base()

         这种方式大幅减轻了继承基类构造函数的书写。另外,由于这是隐式声明继承的,假设一个继承构造函数不被相关的代码使用,编译器不会为之产生真正的函数代码,更加节省目标代码空间。

        缺憾就是不能方便的同时初始化成员,需要手动初始化。

💡 只能引入直接基类的构造函数,例如 class DD : public Derived { using Base::Base; } 是非法的。

引入枚举类型(C++20

        可以通过 using 引入到枚举类型枚举器名称,效果类似于每个枚举器定义在了作用域中,如

enum class fruit { orange, apple };
 
struct S
{
    using enum fruit; // OK: 将 orange 和 apple 引入 S
};
 
void f()
{
    S s;
    s.orange;  // OK: fruit::orange
    S::orange; // OK: fruit::orange
}

         可以通过 using多次引入多个枚举类型,但如果枚举器名称有重复将产生冲突:

enum class fruit { orange, apple };
enum class color { red, orange };
 
void f()
{
    using enum fruit;    // OK
    // using enum color; // error: color::orange and fruit::orange conflict
}

定义别名

定义类型别名

using alias = typename;

       在 C/C++ 中,定义类型别名的方法有 #define、typedefusing 多种方式。

        #define 是宏定义关键字,用途广法,尤其是 C 中,不局限于类型别名定义。

  在C++中,推荐使用 using 来定义类型别名,因为它更符合C++的现代编程风格。

        首先,直观的感受下三者在定义类型别名时的形式,typedef 的方式和其他两者顺序相反:

#define MY_INT int

typedef int MyInt;

using MyInt = int;

         似乎三者没有明显区别,当然由于 #define 宏定义的本质,和后两者是可以明显区分开的。那么,typedef 和 using 的区别在哪里呢?

   首先,使用typedef定义的别名和使用using定义的别名在语义上是等效的。

   然后,我们看下一个定义类型别名的例子:

typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;


using UPtrMap = std::unique_ptr<std::unordered_map<std::string, std::string>>;

         using 的方式是不是更为直观易懂?

        当然,也许你很习惯 typedef 的方式,这个示例还不足以让你倒向 using ,那我们继续。

定义模板类型别名

        typedef 是不支持定义模板类型别名的,例如

template <typename T>
typedef map<int, T> type;	// error, 语法错误

        要实现这一点,需要一个类辅助,这样就很麻烦了。using 可以做到:

template <typename T>
using mymap = map<int, T>;

       typedef 为什么不可以呢,在 n1449 中有这样一段话:

"we specifically avoid the term “typedef template” and introduce the new syntax involving the pair “using” and “=” to help avoid confusion: we are not defining any types here, we are introducing a synonym (i.e. alias) for an abstraction of a type-id (i.e. type expression) involving template parameters."

        所以,这事标准委员会的观点与选择,涉及到模板类型时,我们必须使用 using 
 

总结

        using  关键字为程序书写、定义别名带来了极大的方便,使得代码更为简练,善用 using  ,使得代码更为简洁易懂。

        在 C++11 后续的语言标准中又增加了更多的适用场景如,变量模板(C++14)、类模板的默认实参和推导实参(C++17)、委托转隶构造函数(使用using)(C++20),最后以一段  特性大杂烩作为结束

// 类型别名声明 (C++11)
using IntVector = std::vector<int>;

// 模板别名 (C++11)
template <typename T>
using Pair = std::pair<T, T>;

// 委托构造函数 (C++11)
class Foo {
public:
    using FooBase::FooBase; // 委托给基类的构造函数
};

// 继承构造函数 (C++11)
class Base {
public:
    Base(int i) {}
};

class Derived : public Base {
public:
    using Base::Base; // 使用基类的构造函数
};

// 构造函数的显式使用声明 (C++11)
struct A {
    A(int) {}
};

struct B : A {
    using A::A; // 显式使用A的构造函数
};

// 变量模板 (C++14)
template<typename T>
T template_var = 10;

// 类模板的默认实参和推导实参 (C++17)
template <typename T = int>
struct C {};

// 委托转隶构造函数 (C++20)
struct D : B {
    using B::B; // 委托给另一个构造函数
};

 


参考

C++ keyword: using - cppreference.com

Constructors (C++) | Microsoft Learn

http://isocpp.open-std.org/JTC1/SC22/WG21/docs/papers/2003/n1449.pdf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

0x0007

可不可奖励我吃只毛嘴鸡 馋😋

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值