【Effect C++ 笔记】(四)设计与声明

在这里插入图片描述

【四】设计与声明

条款18 : 让接口容易被正确使用,不易被误用

Item 18: 让接口容易被正确使用,不易被误用
Make interfaces easy to use correctly and hard to use incorrectly.

“让接口容易被正确使用,不易被误用”,这也是面向对象设计中的重要概念,好的接口在工程实践中尤其重要。 在使用优秀的第三方组件时,常常能够切身感受到好的接口原来可以这么方便,甚至不需记住它的名字和参数就能正确地调用。 反观自己写的API,常常会有人三番五次地问这个参数怎么设置,真是失败。人非圣贤孰能无过,只能在这种痛苦的驱动下努力的重构和学习!
虽然我已经脱离了很久的Windows开发,但想起来.NET API良好的设计,还是会五体投地。
言归正传。
在C++中,可以说到处都是接口,接口定义了客户如何与你的代码进行交互。如果用户误用了你的接口,你至少也要承担一部分的责任。 理想情况的接口是这样的: 如果用户误用了接口,代码不会正常编译;如果代码通过了编译,那么你的接口就要完成客户想要的操作。

正确地构造一个Date

来个通俗的例子:Date对象的构造函数需要传入月、日、年。但客户在调用时常常传错顺序,这时可以将参数封装为对象来提供类型检查:

class Date{
public:
Date(const Month& m, const Day& d, const Year& y);
};

Date d(Day(30), Month(3), Year(1995));    // 编译错:类型不兼容!
Date d(Month(3), Day(30), Year(1995));    // OK

即使这样,用户的Month构造函数仍然会传入一个不合理的参数(例如32),或者搞不清楚下标从0还是1开始。 解决方案是预定义所有可用的Month:

class Month{
public:
static Month Jan(){ return Month(1); }
static Month Feb(){ return Month(2); }
};
Date d(Month::Jan(), Day(30), Year(1995));

从上述Date的例子中可以看到,可以将运行时的数据转换为编译期的名称,可以将错误检查提前到编译期。以此解决参数顺序和范围的误用。

限制类型的操作

另外一个例子来自Item 3:尽量使用常量,乘法运算符返回值设为const,以防止误用赋值:

if(a * b = c) ... // 用户的意图本来是判等

提供一致的接口

  • DLL :

image.png
除此之外,提供一致的接口也很重要。例如STL容器封装了互不兼容的基本数据类型,为STL算法提供了非常一致的接口。
比如STL提供了size属性来标识容器的大小,容器可以是数组、链表、字符串、字典、集合。.NET中所有这些大小都叫Count属性。 采用哪种命名并不重要,重要的是提供一致的接口。不仅便于应用中使用,也便于库的扩展。
好的接口不会要求用户去记住某些事情。比如Investment* createInvestment()要求客户记住及时去销毁, 那么客户很可能忘记了去deletedelete了多次。解决方案便是返回一个智能指针而不是原始资源,参见:Item 13:使用对象来管理资源 尤其是当销毁操作不是简单的delete时,客户还需要记住如何去销毁它。而我们返回智能指针时就能指定deleter来自定义销毁动作:

shared_ptr<Investment> createInvestment(){
    // 销毁一项投资时,需要做一些取消投资的业务,而不是简单地`delete`
    return std::tr1::shared_ptr<Investment>(new Stock, getRidOfInvestment);    // tr1::shared_ptr  可以cross-DLL-problem
}

shared_ptr带来的好处还不仅仅是移除了客户的责任,同时还解决了跨DLL动态内存管理的问题。 在DLLnew的对象,如果在另一个DLLdelete往往会发生运行时错误,但shared_ptr进行资源销毁时, 总会调用创建智能指针的那个DLL中的delete,这意味着shared_ptr可以随意地在DLL间传递而不需担心跨DLL的问题。

  • 好的接口容易被正确使用,不易被误用。
  • 促进正确使用的方法包括接口的一致性,以及与内置类型的行为兼容。
  • 可以为内置类型提供一致的接口来方便正确的使用。
  • 识别误用的手段包括: 创建新的类型、限制类型的操作、限制对象的值、移除客户的资源管理责任。
  • shared_ptr 支持定制型删除器, 这可以预防DLL问题, 课内用来自动解除互斥锁 。

条款19 : 设计Class犹如设计type

treat class design as type design

在面向对象语言中,开发者的大部分时间都用在了增强你的类型系统。这意味着你不仅是类的设计者,更是类型设计者。 重载函数和运算符、控制内存分配和释放、定义初始化和销毁操作……良好的类型有着自然的语法、直观的语义,以及高效的实现。 你在定义类时需要像一个语言设计者一样地小心才行!
类的设计就是类型设计,当你定义一个类之前,需要面对这些问题:

  • 这个新的类型如何创建和销毁?new还是new []
  • 初始化和赋值之间又怎样的区别?它们确实是不同的函数调用,参见:Item 4:确保变量的初始化
  • 如果该类型的对象被传值而不是传引用,意味着怎样的语义?记住:传值时调用的是拷贝构造函数!
  • 该类型合理的取值范围是?在你的成员函数、赋值和构造函数中需要做相应的范围检查!
  • 你的新类型能融合到继承图中吗?如果你继承自已有的类,你的类将被它们限制(尤其是虚函数限定);如果你希望其他类来继承该类型,那么你的方法是否需要声明为virtual?尤其是析构函数。
  • 你的新类型允许怎样的类型转换?你可能需要将构造函数声明为explicit来避免隐式类型转换。参见:Item 15:资源管理类需要提供对原始资源的访问
  • 哪些运算符对你的新类型是有意义的?
  • 那些编译器生成的默认方法需要被禁止?参见:Item 6:禁用那些不需要的缺省方法
  • 谁可以访问你的成员方法?私有、保护、共有成员限定符;友元类、友元函数。
  • 你想提供哪些潜在的接口?它们往往关乎异常安全、效率、资源使用等,这些潜在的接口将会影响你的实现。
  • 你的类型有多么通用?如果它是非常通用的类型,你可以考虑通过模板把它定义成一系列的类。
  • 你真的需要这个新的类型吗?如果你为了扩展一个类而继承了它,那么定义一个非成员函数或者模板能否更好地解决问题?

条款20 : 传递常量引用比传值更好

Item 20: Prefer pass-by-reference-to-const to pass-by-value

C++函数的参数和返回值默认采用传值的方式,这一特性是继承自 C 语言的。如果不特殊指定, 函数参数将会初始化为实参的拷贝,调用者得到的也是返回值的一个副本。 这些拷贝是通过调用对象的拷贝构造函数完成的,正是这一方法的调用使得拷贝的代价可能会很高。
通常来讲,传递常量引用比传值更好,同时避免了截断问题。但是内置类型和 STL 迭代器,还是传值更加合适。

来个例子

一个典型的类的层级可能是这样的:

class Person {
string name, address;
};
class Student: public Person {
string schoolName, schoolAddress;
};

假如有这样一处函数调用:

bool validateStudent(Student s);           // function taking a Student by value  传值调用 

Student plato;                             // Plato studied under Socrates
bool platoIsOK = validateStudent(plato);   // call the function

在调用 validateStudent() 时进行了6 个函数调用(Student*1 、 Person * 1 string * 4):

  1. Person 的拷贝构造函数,为什么 Student 的拷贝构造一定要调用 Person 的拷贝构造请参见:Item 12:完整地拷贝对象
  2. Student 的拷贝构造函数
  3. name, address, schoolName, schoolAddress 的拷贝构造函数

解决办法便是传递常量引用:

bool validateStudent(const Student& s);

首先以引用的方式传递,不会构造新的对象,避免了上述例子中 6 个构造函数的调用。 同时 const 也是必须的:传值的方式保证了该函数调用不会改变原来的 Student, 而传递引用后为了达到同样的效果,需要使用 const 声明来声明这一点,让编译器去进行检查!

截断问题

将传值改为传引用还可以有效地避免 截断问题:由于类型限制,子类对象被传递时只有父类部分被传入函数。
比如一个 Window 父类派生了子类 WindowWithScrollBars

class Window {
public:
...
std::string name() const;           // return name of window
virtual void display() const;       // draw window and contents
};

class WindowWithScrollBars: public Window {
public:
...
virtual void display() const;
};

有一个访问 Window 接口的函数,通过传值的方式来获取 Window 的实例:

// incorrect! parameter may be sliced!
void printNameAndDisplay(Window w){     
    std::cout << w.name();
    w.display();
}

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

当调用 printNameAndDisplay 时参数类型从 WindowWithScrollBars 被隐式转换为 Window。 该转换过程通过调用 Window 的拷贝构造函数来进行。 导致的结果便是函数中的 w 事实上是一个 Window 对象, 并不会调用多态子类 WindowWithScrollBarsdisplay()

// fine, parameter won't be sliced
void printNameAndDisplay(const Window& w){ 
    std::cout << w.name();
    w.display();
}

这就很好嘛,如果你曾深入过编译器你会发现引用是通过指针来实现的。

特殊情况

  • “小型的用户自定义类型也没必要用pass-by-value

一般情况下相比于传递值,传递常量引用是更好的选择。但也有例外情况,比如 内置类型STL 迭代器和函数对象。
内置类型传值更好是因为它们小,而一个引用通常需要 32 位或者 64 位的空间。可能你会认为小的对象也应当首选传值, 但 对象小并不意味着拷贝构造的代价不高!比如 STL 容器通常很小,只包含一些动态内存的指针。然而它的拷贝构造函数中, 必然会分配并拷贝那些动态内存的部分。
即使拷贝构造函数代价很小,传值的方式仍然有性能问题。有些编译器会区别对待内置类型和用户定义类型, 即使它们有相同的底层表示。比如有些编译器虽然会把 double 放入寄存器,但是拒绝将只含一个 double 的对象放入寄存器。
一个只含 double 的对象大小为 8,它和一个 double 具有相同的大小和底层表示。关于对象大小的计算,请参考:Item 7:将多态基类的析构函数声明为虚函数
从面向对象设计方面来讲,即使对象现在很小,但它作为用户定义类型是有可能变大的(如果你更改了内部实现)。 从长远来讲的性能考虑,也应当采取传引用的方式来设计使用它的函数。
STL 迭代器和函数对象也应当被传值,这是因为它们在 STL 中确实是被这样设计的,同时它们的拷贝构造函数代价并不高。

条款21 : 需要返回对象时,不要返回引用

Item 21: Don’t try to return a reference when you must return an object

Item 20 中提到,多数情况下传引用比传值更好。追求这一点是好的,但千万别返回空的引用或指针。 一个典型的场景如下:

class Rational{
    int n, d; // n : 分子   d : 分母 
public:
    Raitonal(int numerator=0, int denominator=1);
};

// 返回值为什么是const请参考Item 3
friend const Rational operator*(const Rational& lhs, const Rational& rhs);

Rational a, b;
Rational a(1, 2); // 1/ 2
Rational b(3, 5);  // 3/ 5
Rational c = a*b;   //理论来说是 3 / 10 

C11 move 语义为这种情况提供了更好的支持 .
注意operator*返回的是Rational实例,a*b时便会调用operator*( ), 返回值被拷贝后用来初始化c。这个过程涉及到多个构造和析构过程:

  1. 函数调用结束前,返回值被拷贝,调用拷贝构造函数
  2. 函数调用结束后,返回值被析构
  3. c 被初始化,调用拷贝构造函数
  4. c 被初始化后,返回值的副本被析构

我们能否通过传递引用的方式来避免这些函数调用?这要求在函数中创建那个要被返回给调用者的对象,而函数只有两种办法来创建对象:在栈空间中创建、或者在堆中创建。在栈空间中创建显然是错误的:

const Rational& operator*(const Rational& lhs, const Rational& rhs){
    Rational result(lhs.n*rhs.n, lhs.d*rhs.d); // 糟糕的写法 : on-the-stack
    return result;
}

客户得到的result永远是空。因为引用只是一个名称,当函数调用结束后
result即被销毁。 它返回的是一个ex-result的引用。那么在堆中创建会是怎样的结果?

const Rational& operator*(const Rational& lhs, const Rational& rhs){
    Rational *result  = new Rational(lhs.n*rhs.n, lhs.d*rhs.d);  //更糟糕的写法 : on-the=heap
    return *result;
}

问题又来了,既然是new的对象,那么谁来delete呢?比如下面的客户代码:

Rational w, x, y, z;
w = x*y*z;

上面这样合理的代码都会导致内存泄露,那么operator*的实现显然不够合理。此时你可能想到用静态变量来存储返回值,也可以避免返回值被再次构造。但静态变量首先便面临着线程安全问题,除此之外当客户需要不止一个的返回值同时存在时也会产生问题:

const Rational& operator*(const Rational& lhs, const Rational& rhs){
    static Rational result ;
    result = lhs * rhs;  // 普通人写法    但是这样会调用构造和析构 
    return *result;
}

if((a*b) == (c*d)){
    // ...  调用3次析构 + 构造   (传递的都是 “现值”)
}

如果operator*的返回值是静态变量,那么上述条件判断恒成立,因为等号两边是同一个对象嘛。如果你又想到用一个静态变量的数组来存储返回值,那么我便无力吐槽了。。。
挣扎了这许多,我们还是返回一个对象吧:

inline const Rational operator*(const Rational& lhs, const Rational& rhs){
    return Rational(lhs.n*rhs.n, lhs.d*rhs.d);
}

事实上拷贝构造返回值带来的代价没那么高,C++标准允许编译器做出一些客户不可察觉(without changing observable behavior)的优化。在很多情况下,返回值并未被析构和拷贝构造。
永远不要返回局部对象的引用或指针或堆空间的指针,如果客户需要多个返回对象时也不能是局部静态对象的指针或引用。 Item 4:确保变量的初始化 指出,对于单例模式,返回局部静态对象的引用也是合理的。

总结: 根据工作需要,挑选返回 reference or object , 目的就是让编译器厂商为尽可能降低成本 !!

条款22 : 数据成员应声明为私有

Item 22: Declare data members private

数据成员声明为私有可以提供一致的接口语法,提供细粒度的访问控制,易于维护类的不变式,同时可以让作者的实现更加灵活。而且我们会看到,protected并不比public更加利于封装。

语法一致性

你肯定也遇到过这种困惑,是否应该加括号呢?

obj.length  // 还是 obj.length()?
obj.size    // 还是 obj.size()?

总是难以记住如何获取该属性,是调用一个getter?还是直接取值?如果我们把所有数据都声明为私有,那么在调用语法上,统一用括号就好了。

访问控制

为数据成员提供gettersetter可以实现对数据更加精细的访问控制,比如实现一个只读的属性:

class readOnly{
int data;
public:
int get() const { return data; }
}

事实上,在C#中提供了访问器(又称属性)的概念, 每个数据成员都可以定义一套访问器(包括settergetter),使用访问器不需要使用括号:

public class readWrite{
private string _Name;
public string Name{
    set { this._Name = value; }
get { return this._Name; }
}
}
ReadWrite rw;
// 将会调用set方法
rw.Name = "alice";

可维护性

封装所有的数据可以方便以后类的维护,比如你可以随意更改内部实现,而不会影响到既有的客户。例如一个SpeedDataCollection需要给出一个平均值:

class SpeedDataCollection{
public:
    void add(int speed);
    double average() const;
};

average()可以有两种实现方式:①维护一个当前平均值的属性,每当add时调整该属性;②每次调用average()时重新计算。两种实现方式之间的选择事实上是CPU和内存的权衡,如果在一个内存很有限的系统中可能你需要选择后者,但如果你需要频繁地调用average()而且一点内存不是问题,那么就可以选择前者。
你的实现方式的变化不会影响到你的客户。但如果avarage()不是方法而是一个共有数据成员。 那么对于你的每次实现方式变化,客户就必须重新实现、重新编译、重新调试和测试它们的代码了。

来看看 protected

既然共有数据成员会破坏封装,它的改动会影响客户代码。那么protected呢?
面向对象的精髓在于封装,可以粗略地认为一个数据成员的封装性反比于它的改动会破坏的代码数量。比如上述的average如果是一个public成员,它的改动会影响到所有曾使用它的客户代码,它们的数量是大到不可知的(unknowably large amount)。如果是protected成员,客户虽然不能直接使用它,但可以定义大量的类来继承自它,所以它的改动最终影响的代码数量也是 unknowably large
protectedpublic的封装性是一样的!如果你曾写了共有或保护的数据成员,你都不能随意地改动它们而不影响客户代码!

  • 切记将成员变量声明为private, 这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证, 给class实现提供弹性。、
  • protested 并不比 public、 更具有封装性

条款 23 : 非成员非友元函数好于成员函数

Item 23: Prefer non-member non-friend functions to member functions

class WebBrowser{
    public:
    void clearCache();
    void clearCookies();
    void clearHistory();
};

此时你要实现一个clearEverything()有两种方式:

class WebBrowser{
public:
    void clearEverything(){
        clearCache();
        clearCookies();
        clearHistory();
    }
}
// 或者使用非成员函数:
void clearEverything(WebBrowser& wb){
    wb.clearCache();
    wb.clearCookies();
    wb.clearHistory();
}

哪种更好呢?面向对象原则指出,数据和数据上的操作应当绑定在一起,那么前者更好。 这是对面向对象的误解,面向对象设计的精髓在于封装,数据应当被尽可能地封装。 相比于成员函数,非成员函数提供了更好的封装,包的灵活性(更少的编译依赖),以及功能扩展性。

封装性

封装就是对外界隐藏的意思。如果数据被越好地封装,那么越少的东西可以看到它,我们便有更大的灵活性去改变它。这是封装带来的最大的好处:给我们改变一个东西的灵活性,这样的改变只会影响到有限的客户。
作为粗粒度的估计,数据的封装性反比于可访问该数据的函数数量。这些函数包括成员函数、友元函数和友元类中的函数。 因此非成员非友元函数会比成员函数提供更好的封装, 我们应该选择clearEverything()的第二种实现。
Item22提到,如果一个数据成员不是私有的,那么将会有无限数量的函数可访问它。
这里有两点值得注意:

  1. 友元函数成员函数一样的,因为友元函数也可以访问私有数据成员,它和成员函数对封装具有相同的影响。
  2. 非成员函数并不意味着它不可以是其他类的成员函数。尤其是在像Java,C#之类的语言中,函数必须定义在类中。
  3. 静态成员函数也是不错的选择。因为静态函数不能访问对象成员,因此不会影响对象的封装。

扩展性

C++中,可以把这些非成员函数定义在相同的命名空间下。 但问题又来了:这些在命名空间下的函数并不在类中,它们会被传播到所有的源文件中。 而客户并不希望为了使用几个工具函数,就对这样一个庞大的命名空间产生编译依赖。 因此我们可以将不同类别的工具函数放在不同的头文件中,客户可以选择它想要的那部分功能:

// file: webbrowser.h
namespace WebBrowserStuff{
    class WebBrowser{};
}

// file: webbrowser-bookmarks.h
namespace WebBrowserStuff{
    ...
        }

// file: webbrowser-cookies.h
namespace WebBrowserStuff{
    ...
        }

这也是C++标准库的组织方式,std命名空间下的所有东西都被分在了不同的头文件中:<vector>, <algorithem>, <memory>等。这样客户代码只对它引入的那部分功能产生编译依赖。 为了做到这一点,这些工具函数必须是非成员函数,因为类作为整体必须在一个文件中进行定义。
同一命名空间不同头文件的组织方式,也为客户扩展工具函数提供了可能。 客户可以在同一命名空间下定义他自己的工具函数, 这些函数便会和既有工具函数天然地集成在一起,如 用户使用vector 的话 不需要 include <list> <memory>。 这也是成员函数无法做到的一个特性,因为类的定义对客户扩展是关闭的。 即使是子类也不能访问封装的(私有)成员数据, 况且有些类不是用来做基类的(见Item 7:将多态基类的析构函数声明为虚函数)。

非成员非友元函数好于成员函数 : 这样可以增加封装性、包裹弹性和机能扩充性。

条款24 若所有参数都需要类型装换,请为此采用non-member函数

Item 24: Declare non-member functions when type conversions should apply to all parameters.

虽然Item 15:资源管理类需要提供对原始资源的访问中提到,最好不要提供隐式的类型转化。 但这条规则也存在特例,比如当我们需要创建数字类型的类时。正如double和int能够自由地隐式转换一样, 我们的数字类型也希望能够做到这样方便的接口。 当然这一节讨论的问题不是是否应当提供隐式转换,而是如果运算符的所有“元”都需要隐式转换时,请重载该运算符为友元函数。
通过运算符重载来扩展用户定义类型时,运算符函数可以重载为成员函数,也可以作为友元函数。 但如果作为了成员函数,this将被作为多元操作符的第一元,这意味着第一元不是重载函数的参数,它不会执行类型转换。 仍然拿有理数类作为例子,下面的Rational类中,将运算符重载为成员函数:

class Rational{
public: 
Rational(int n = 0, int d = 1);
int numerator() const;
int denominator() const;
const Rational operator*(const Rational& rhs) const;
...

我们看下面的运算符调用能否成功:

Rational oneHalf(1, 2);

Rational result = oneHalf * oneHalf;   // OK
result = oneHalf * 2;                  // OK
result = 2 * oneHalf;                  // Error

第一个运算符的调用的成功是很显然的。我们看第二个调用:
当编译器遇到运算符*时,它会首先尝试调用:

result = oneHalf.operator*(2);

编译器发现该函数声明(它就是定义在Rational类中的方法)存在, 于是对参数2进行了隐式类型转换(long->Rational)。所以第二个调用相当于:

Rational tmp(2);
result = oneHalf.operator*(tmp);

将Rational的构造函数声明为explicit可以避免上述隐式转换,这样第二个调用也会失败。
对于第三个调用,编译器仍然首先尝试调用:

result = 2.operator*(oneHalf);

2属于基本数据类型,并没有成员函数operator*。于是编译器再尝试调用非成员函数的运算符:

result = operator*(2, oneHalf);

再次失败,因为并不存在与operator*(long, Rational)类型兼容的函数声明,所以产生编译错误。 但如果我们提供这样一个非成员函数:

const Rational operator*(const Rational& lhs, const Rational& rhs);

这时候第一个参数也可以进行隐式转换。第三个调用(result = 2 * oneHalf)便会成功,该表达式相当于:

Rational tmp(2);
result = operator*(tmp, oneHalf);

只有当运算符的元出现在运算符函数的参数列表时,它才会被隐式类型转换。所以当我们需要运算符的所有“元”都可以被隐式转换时, 应当将运算符声明为非成员函数。 在JavaScript或者C#中,这个规则是不需要的,因为编译器/解释器在这里做了更多的工作。比如JavaScript中2.toFixed(3) 会被解释为Number(2).toFixed(3),该表达式的值为"2.000"。

条款25 : 考虑写出一个不抛异常的swap函数

Item 25: Consider support for a non-throwing swap.

swap 函数最初由 STL 引入,已经成为异常安全编程(见 Item 29)的关键函数, 同时也是解决自赋值问题(参见 Item 11:赋值运算符的自赋值问题)的通用机制。 std 中它的基本实现是很直观的:

namespace std{
    template<typename T>
    void swap(T& a, T& b){
        T tmp(a);
        a = b;
        b = tmp;
    }
}

可以看到,上述 swap 是通过赋值和拷贝构造实现的。所以 std::swap 并未提供异常安全, 但由于 swap 操作的重要性,我们应当为自定义的类实现异常安全的 swap,这便是本节的重点所在。

类的 swap

先不提异常安全,有时 std::swap 并不高效(对自定义类型而言)。 比如采用 pimpl idiom(见 Item 31)设计的 类 中,实际上只需要交换对应的指针即可实现 swap :

class WidgetImpl;
class Widget {           // pimpl idiom 的一个类
	WidgetImpl *pImpl;   // 指向Widget的实现(数据)        
public:
    Widget(const Widget& rhs);
}; 

namespace std {
    template<>                      // 模板参数为空,表明这是一个全特化
    void swap<Widget>(Widget& a, Widget& b){   
        swap(a.pImpl, b.pImpl);     // 只需交换它们实体类的指针 
    }
}

上述代码是不能编译的,因为 pImpl 是私有成员!所以,Widget 应当提供一个 swap 成员函数或友元函数。 惯例上会提供一个成员函数:

class Widget {
public:       
    void swap(Widget& other){
        using std::swap;          // 为何要这样?请看下文
        swap(pImpl, other.pImpl);
    }
};

接着我们继续特化 std::swap,在这个通用的 swap 中调用那个成员函数:

namespace std {
    template<>
    void swap<Widget>(Widget& a, Widget& b){
        a.swap(b);              // 调用成员函数
    }
}

到此为止,我们得到了完美的 swap 代码。上述实现与 STL 容器是一致的:提供公有 swap 成员函数, 并特化 std::swap 来调用那个成员函数。

类模板的 swap

当 Widget 是类模板时,情况会更加复杂。按照上面的 swap 实现方式,你可能会这样写:

template<typename T>
class WidgetImpl { ... };

template<typename T>
class Widget { ... };

namespace std {
    template<typename T>
    // swap 后的尖括号表示这是一个特化,而非重载。
    // swap<> 中的类型列表为 template<> 中的类型列表的一个特例。
    void swap<Widget<T>>(Widget<T>& a, Widget<T>& b){
        a.swap(b); 
    }
}

悲剧的是上述代码不能通过编译。C++ 允许偏特化类模板,却不允许偏特化函数模板(虽然在有些编译器中可以编译)。 所以我们干脆不偏特化了,我们来重载 std::swap 函数模板:

namespace std {
    template<typename T>
    // 注意 swap 后面没有尖括号,这是一个新的模板函数。
    // 由于当前命名空间已经有同名函数了,所以算函数重载。
    void swap(Widget<T>& a, Widget<T>& b){
        a.swap(b); 
    }
}

这里我们重载了 std::swap,相当于在 std 命名空间添加了一个函数模板。但这在 C++ 标准中是不允许的! C++ 标准中,客户只能特化 std 中的模板,但不允许在 std 命名空间中添加任何新的模板。 上述代码虽然在有些编译器中可以编译,但会引发未定义的行为,所以不要这么搞!
那怎么搞?办法也很简单,就是别在 std 下添加 swap 函数了,把 swap 定义在 Widget 所在的命名空间中:

namespace WidgetStuff {
    template<typename T> 
    class Widget { ... };

    template<typename T> 
    void swap(Widget<T>& a, Widget<T>& b){
        a.swap(b);
    }
}

任何地方在两个 Widget 上调用 swap 时,C++根据其 argument-dependent lookup(又称 Koenig lookup) 会找到 WidgetStuff 命名空间下的具有 Widget 参数的 swap。
那么似乎 类的 swap 也只需要在同一命名空间下定义 swap 函数,而不必特化 std::swap。 但是!有人喜欢直接写 std::swap(w1, w2),特化 std::swap 可以让你的类更加健壮。
因为指定了调用 std::swap,argument-dependent lookup 便失效了,WidgetStuff::swap 不会得到调用。
说到这里,你可能会问如果我希望优先调用 WidgetStuff::swap,如果未定义则取调用 std::swap,那么应该如何写呢? 看代码:

template<typename T>
void doSomething(T& obj1, T& obj2){
    using std::swap;           // 使得`std::swap`在该作用域内可见
    swap(obj1, obj2);          // 现在,编译器会帮你选最好的swap
}

此时,C++编译器还是会优先调用指定了 T 的 std::swap,其次是 obj1 的类型 T 所在命名空间下的对应 swap 函数, 最后才会匹配 std::swap 的默认实现。

最佳实践

如何实现 swap 呢?总结一下:

  1. 提供一个更加高效的,不抛异常的公有成员函数(比如 Widget::swap)。
  2. 在你类(或类模板)的同一命名空间下提供非成员函数 swap,调用你的成员函数。
  3. 如果你写的是类而不是类模板,也可以特化 std::swap,同样地在里面调用你的成员函数。
  4. 调用时,请首先用 using 使std::swap可见,然后直接调用 swap

参考

  • https://harttle.land/effective-cpp.html

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Abaqus2023新功能:分析技术

隐式耦合的松弛和加速器方法 产品&#xff1a;Abaqus/Standard SIMULIA协同仿真引擎现在支持Aitkens松弛方法以及Anderson和Broyden加速器方法&#xff0c;为强耦合物理场提供稳健且省时高效的解决方案。此功能在 2022 FD04 &#xff08;FP.2232&#xff09;版本中首次提供。…

vue的常用指令

1.使用双花括号( {{}} )对变量输出,内部可以写简单的表达式用于对数据的处理 2..v-text&#xff1a;相当于js的innerText, 3.v-html&#xff1a;相当于js的innerHTML 4.v-bind&#xff1a;动态绑定属性,简写是冒号( : ) 5.绑定class&#xff1a;操作元素的 class 列表和内联样式…

Moka人事:实现无代码开发的API连接,打通电商平台与用户运营系统

无代码开发的API连接&#xff1a;Moka人事的核心优势 Moka人事&#xff0c;是北京希瑞亚斯科技有限公司于2015年推出的一款数据驱动的智能化HR SaaS产品。这款产品的主要优势在于其无需进行API开发即可实现系统的连接和集成&#xff0c;这不仅大大提升了企业的工作效率&#x…

2023数维杯国际赛数学建模D题思路模型分析

D题思路模型分析&#xff1a;详细思路获取见文末名片 问题D&#xff1a;洗衣清洗的数学问题 洗衣清洗是人们每天都在做的事情。洗衣粉的去污功能来自于一些表面活性剂的化学物质。它们可以提高水的渗透性&#xff0c;并利用分子间静电排斥机制去除污垢颗粒。由于表面活性剂分…

【汇编】Debug的使用

文章目录 前言一、Debug是什么&#xff1f;二、为什么Debug如此重要&#xff1f;三、Debug的使用3.1 Debug的运行3.1 R命令查看寄存器的状态改变寄存器的值 3.2 用D命令查看内存中的内容列出预设地址内存内容列出指定地方的内容列出指定地方的指定大小的内容 3.3 使用e命令修改…

learning to rank 学习排名系统综述

Learning to Rank 的实践 文档列表方法 Listwise 算法相对于 Pointwise 和 Pairwise 方法来说&#xff0c;它不再将排序问题转化为一个分类问题或者回归问题&#xff0c;而是直接针对评价指标对文档的排序结果进行优化&#xff0c;如常用的 MAP、NDCG 等。应用 Listwise 的模型…

js构造函数之工厂模式(学习笔记1)

目录 一、简单工厂 1、存储一个用户信息 2、存储N个用户信息 3、存储N个用户信息不同年龄用户有不同美食的搭配方案【简单工厂模式】 二、抽象工厂模式 1、抽象工厂(AbstractFactory) 2、具体工厂&#xff08;ConcreteFactory&#xff09; 3、生产新款手机 4、总结 本…

V10服务器安装virt-manage

kvm是什么 KVM(Kernel-based Virtual Machine, 即内核级虚拟机) 是一个开源的系统虚拟化模块。它使用Linux自身的调度器进行管理&#xff0c;所以相对于Xen&#xff0c;其核心源码很少。目前KVM已成为学术界的主流VMM之一&#xff0c;它包含一个为处理器提供底层虚拟化 可加载…

服务器数据恢复—服务器raid5离线磁盘上线同步失败的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 某品牌DL380服务器中有一组由三块SAS硬盘组建的RAID5阵列。数据库存放在D分区&#xff0c;数据库备份存放在E分区。 服务器上有一块硬盘的状态灯显示红色&#xff0c;D分区无法识别&#xff0c;E分区可识别&#xff0c;但是拷贝文件报…

MyBatis 操作数据库(⼊⻔)

前言 通过本篇博客&#xff0c;我们将学到以下内容 1.使⽤MyBatis完成简单的增删改查操作,参数传递 2.掌握MyBatis的两种写法: 注解和 XML⽅式 3.掌握 MyBatis 相关的⽇志配置 什么是 MyBatis? MyBatis是⼀款优秀的 持久层 框架&#xff0c;⽤于简化JDBC&#xff08;关于 JD…

推荐5款堪称神器的免费软件

​ 今天再次推荐5个良心好用的Windows神级软件&#xff0c;每一个都是完全免费&#xff0c;堪称神器&#xff0c;让你打开新世界的大门。 1.文件复制——SuperCopy ​ SuperCopy 是一款 Chrome 浏览器的扩展&#xff0c;可以帮助您解除网站上禁止复制、右键、全选、粘贴等限制…

Linux Docker图形化工具Portainer如何进行远程访问?

文章目录 前言1. 部署Portainer2. 本地访问Portainer3. Linux 安装cpolar4. 配置Portainer 公网访问地址5. 公网远程访问Portainer6. 固定Portainer公网地址 前言 Portainer 是一个轻量级的容器管理工具&#xff0c;可以通过 Web 界面对 Docker 容器进行管理和监控。它提供了可…

easyExcle单元格合并

自定义单元格合并策略&#xff1a; /*** 自定义单元格合并策略** create: 2023-11-15 13:41**/ Data NoArgsConstructor AllArgsConstructor Slf4j public class EasyExcelCustomMergeStrategy implements RowWriteHandler {/*** 总数*/private Integer totalNum;//合并行计数…

填充每个节点的下一个右侧节点指针

题目链接 填充每个节点的下一个右侧节点指针 题目描述 注意点 给定一个 完美二叉树 解答思路 广度优先遍历一层层的遍历二叉树&#xff0c;将每一层节点的next指针都指向右侧节点 代码 class Solution {public Node connect(Node root) {if (root null) {return null;}…

[nlp] 损失缩放(Loss Scaling)loss sacle

在深度学习中,由于浮点数的精度限制,当模型参数非常大时,会出现数值溢出的问题,这可能会导致模型训练不稳定。为了解决这个问题,损失缩放(Loss Scaling)技术被引入,它通过缩放损失值来解决这个问题。 在深度学习中,损失缩放技术通常是通过将梯度进行缩放来实现的。具…

【ES6标准入门】JavaScript中的模块Module语法的使用细节:export命令和imprt命令详细使用,超级详细!!!

&#x1f601; 作者简介&#xff1a;一名大四的学生&#xff0c;致力学习前端开发技术 ⭐️个人主页&#xff1a;夜宵饽饽的主页 ❔ 系列专栏&#xff1a;JavaScript进阶指南 &#x1f450;学习格言&#xff1a;成功不是终点&#xff0c;失败也并非末日&#xff0c;最重要的是继…

Google codelab WebGPU入门教程源码<5> - 使用Storage类型对象给着色器传数据(源码)

对应的教程文章: https://codelabs.developers.google.com/your-first-webgpu-app?hlzh-cn#5 对应的源码执行效果: 对应的教程源码: 此处源码和教程本身提供的部分代码可能存在一点差异。运行的时候&#xff0c;点击画面可以切换效果。 class Color4 {r: number;g: numb…

Java面向对象(高级)-- static关键字的使用

文章目录 一、static关键字&#xff08;1&#xff09;类属性、类方法的设计思想&#xff08;2&#xff09; static关键字的说明&#xff08;3&#xff09;static修饰属性1. 复习变量的分类2. 静态变量2.1 语法格式2.2 静态变量的特点2.3 举例2.3.1 举例12.3.2 举例22.3.3 举例3…

linux套接字-Socket

1.概念 局域网和广域网 局域网&#xff1a;局域网将一定区域内的各种计算机、外部设备和数据库连接起来形成计算机通信的私有网络。广域网&#xff1a;又称广域网、外网、公网。是连接不同地区局域网或城域网计算机通信的远程公共网络。IPInternet Protocol&#xff09;&#…

无需云盘,不限流量实现Zotero跨平台同步:内网穿透+私有WebDAV服务器

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 无需云盘&#xff0c;不限流量实现Zotero跨平台同步&#xff1a;内网穿透私有WebDAV服务器 文章目…