设计与声明
让接口容易被使用,不易被误用
用户可能犯什么错误
-
错误类型
- 以错误的次序传递参数
- 可能传递无效的参数
原则
-
让类型容易被正确使用,不容易被误用:除非有好理由,否则应该尽量令你的types 行为与内置types 一致
- 比如,一个容器,通过size()方法统一得到元素个数是最理想的方法,STL 中
-
任何接口如果要求客户必须计得做某些事情,就是有着“不正确使用”的倾向
- 前面的工厂类,返回的指针类型,要求,客户调用delete 方法,理想情况是工厂方法返回的直接就是shared_ptr 智能指针,此时客户的操作更加安全,因为不怕忘记释放内存
-
"促进正确使用“的办法包括接口的一致性,以及与内置类型的行为兼容
-
”阻止误用“的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
-
tr1::shared_ptr 支持定制型删除器,这可防范DLL 问题,可悲用来自动解除互斥锁等
- 另外,shared_ptr 可以指定,删除器,这样给资源的释放带来了更大的灵活性,客户端方完全不需要了解内部的细节
设计class 犹如设计type
新type 的对象应该如何被创建和销毁
对象的初始化和对象的赋值有什么差别
新的type 的对象如果被passed by value 意味着什么
什么新type 的”合法值“
你的新type 需要配合某个继承图系吗
你的新type 需要什么样的转换
什么样的操作符和函数对此新type 而言是合理的
什么样的标准函数应该驳回,哪些是你必须声明为private者
谁该取用新type 的成员,哪个是public
什么是新type 的”未声明接口“,它对效率、异常安全性、资源运用提供何种保证?
新type 有多么一般化
- 使用可以建立一个模板体系,例如vector
是否真的需要一个新type?
宁愿以pass-by-reference-to-const 替换pass-by-value
引用通常更加高效,可避免切割问题(如果by value,将子类对象传递给需要父类对象的函数,产生对象切割)
并不一定适用于内置类型(更直接、安全、高效、简单),以及STL 的迭代器和函数对象,对它们,pass-by-value 更恰当
必须返回对象时,别妄想返回其引用
比如一个复数类,的*操作符,以by value 的形式返回了得到的结果
-
返回引用似乎效率更高?
-
引用的问题
- 它的另一个名称是什么
- 必须是对于存在的对象的引用
-
函数创建新对象的两种方式
-
栈空间
- 函数返回后销毁
-
堆空间
-
调用者要负责释放该内存
- Rational w,x,y,z;
w = xyz;//此时很容易内存泄漏
- Rational w,x,y,z;
-
-
全局空间,静态变量
-
需要考虑多线程问题、对程序的性能影响问题
- 单纯的单个静态变量有明显的多线程问题
- if (operator==(operator*(a,b),operator*(c,d)))//此时,返回的是同样的静态变量,很难办
- 如果使用一个,静态变量数组,同时给每个新的线程一个新分配的静态变量使用,想着这一套机制就很复杂,更容易出错
-
-
-
总的来说,此时,以by value 的形式返回更加高效,引用的方式返回更加繁琐而且有潜在的问题
-
绝对不要返回指针或引用指向一个本地栈对象,或返回引用指向一个堆申请的对象,或返回指针或引用指向一个本地静态对象而有可能同时需要多个这样的对象
将成员变量声明为private
语法一致性
- 如果成员变量不是public,客户唯一能够访问对象的方法就是通过成员函数。
- 如果public 接口内的每样东西都是函数,客户就不需要在打算访问类成员时疑惑是否应该使用函数形式
函数可以让设计者对成员变量的处理有更加精确的控制,(读写访问权限)
函数给类带来了更大的封装性
- 客户不关心内部的实现、只关心其提供的功能,面向接口编程
- 实现者扩展或修改内部实现时,客户端无需修改
public 意味着不封装,不封装意味着不灵活,耦合性大、扩展功能时成本太高
protected
- 对外部封装,对子类放开,其实也是一种比较差的封装
- 某些东西的封装新与”当其内容改变时可能造成的代码破坏量“成反比
原则
- 将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class 作者以充分的实现弹性
- protected 并不比public 更具封装性
宁愿以non-member、non-friend替换member函数
封装
- 越多的东西被封装,我们改变这些东西的能力也就越大,它使得我们能够改变事物而只影响有限客户
对象内数据
- 越少代码可以访问它,越多的数据可以被封装,而我们越能自由的改变对象数据-----越多函数可以访问它,数据的封装性越低
- 如果一个功能,同时可以由成员函数、友元函数、非成员非友元函数间做选择。最好的选择是:non-member non-frient 函数,它不增加”能够访问class 内私有成分“的函数数量
我们可以将这种对于类的功能的二次开发(假如它利用基本函数实现了特定功能集),放到一个命名空间中。
-
命名空间可以跨越多个源码文件
- std 命名空间的实现就处于多个源文件
宁可拿non-member non-friend 函数替换member 函数,这样可增加封装性
若所有参数都需要类型转换,为此采用non-member 函数
Rational 为有理数
-
Rational(int numerator=0,//构造函数刻意不为explicit;
int denominator=1);//语序int to Rational 隐式转换 -
有理数*int 和 int * 有理数应该具有同样的结果
-
public:
const Rational operator*(const Rational& rhs) const;-
Rational * int 此时合法
- 发生了所谓隐式类型转换,编译器知道你正在传递一个int,而函数需要的是Rational,它也知道调用Rational构造函数并赋予你所提供的int,就可以符合operator*
-
int*Rational 不合法
-
只有当参数被列入参数列内,这个参数才是隐式类型转换的合格参与者。
- 第一个调用,包含一个Rational 为第一个参数,其即成员操作符函数* 运算的第一个参数
- 因此,修改办法是让operator* 成为一个non-member 函数,以允许编译器在每一个实参身上执行隐式类型转换
-
-
考虑写出一个不抛出异常的swap 函数
#include <iostream>
using namespace std;
#define SHOW cout << a << endl; \
cout << b << endl;
int main()
{
int a = 1;
int b = 2;
SHOW
std::swap<int>(a, b);//std::swap(a,b); 也是直接可以的,自动生成
SHOW
return 0;
}
std::swap 用法示例如上,默认实现如下:
template<class _Ty,
class> inline
void swap(_Ty& _Left, _Ty& _Right)
_NOEXCEPT_COND(is_nothrow_move_constructible_v<_Ty>
&& is_nothrow_move_assignable_v<_Ty>)
{ // exchange values stored at _Left and _Right
_Ty _Tmp = _STD move(_Left);
_Left = _STD move(_Right);
_Right = _STD move(_Tmp);
}
简单的,swap 而已,没有什么特别的。如何定制?
#include <iostream>
using namespace std;
#define SHOW cout << static_cast<void*>(temp1.getPsz()) << endl; \
cout << static_cast<void*>(temp2.getPsz()) << endl;
class CHAR_OP
{
public:
CHAR_OP() :psz(NULL) {}
CHAR_OP(char* in_str) :psz(in_str) { }
~CHAR_OP() { if (psz) { delete[] psz; psz = NULL; } }
void swap(CHAR_OP & other)
{
using std::swap; // 指示我们使用std中的swap,而且是CHAR_OP 独有的版本
swap(psz, other.psz); // 使用swap 的内部实现
}
char* getPsz() { return psz; }
private:
char* psz;
};
namespace std {
template<> // 特例化我们的swap 实现
void swap<CHAR_OP>(CHAR_OP& a, CHAR_OP& b)
{
a.swap(b);//如果要置换CHAR_OP 调用其swap 函数
}
}
int main()
{
CHAR_OP temp1(new char[4]);
CHAR_OP temp2(new char[4]);
SHOW
std::swap<CHAR_OP>(temp1, temp2);//std::swap(temp1,temp2);// 也可以自动识别
SHOW
getchar();
getchar();
return 0;
}
当我们的CHAR_OP 中的指针类型,为自定义,即模板类型,我们的操作又将如何?
template<class T>
class CHAR_OP
{
public:
CHAR_OP() :psz(NULL) {}
CHAR_OP(char* in_str) :psz(in_str) { }
~CHAR_OP() { if (psz) { delete[] psz; psz = NULL; } }
void swap(CHAR_OP & other)
{
using std::swap; // 指示我们使用std中的swap,而且是CHAR_OP 独有的版本
swap(psz, other.psz); // 使用swap 的内部实现
}
T* getPsz() { return psz; }
private:
T* psz;
};
namespace std {
template<typename T> // 特例化我们的swap 实现
void swap<CHAR_OP<T>>(CHAR_OP<T>& a, CHAR_OP<T>& b)
{
a.swap(b);//如果要置换CHAR_OP 调用其swap 函数
}
}
编译器报错
- error C2768: “std::swap”: 非法使用显式模板参数
我们无法添加新的templates 到std 里面。这里的替代方法是,将类与swap放到独立的命名空间:
#include <iostream>
using namespace std;
#define SHOW cout << static_cast<void*>(temp1.getPsz()) << endl; \
cout << static_cast<void*>(temp2.getPsz()) << endl;
namespace MyNamespace {
template<typename T>
class T_OP
{
public:
T_OP() :psz(NULL) {}
T_OP(T* in_str) :psz(in_str) { }
~T_OP() { if (psz) { delete[] psz; psz = NULL; } }
void swap(T_OP & other)
{
//using std::swap; // 指示我们使用std中的swap,而且是CHAR_OP 独有的版本
std::swap(psz, other.psz); // 使用swap 的内部实现
}
T* getPsz() { return psz; }
private:
T* psz;
};
template<typename T> // 特例化我们的swap 实现
void swap(T_OP<T>& a, T_OP<T>& b)
{
a.swap(b);//如果要置换CHAR_OP 调用其swap 函数
}
}
int main()
{
MyNamespace::T_OP<char> temp1(new char[4]);
MyNamespace::T_OP<char> temp2(new char[4]);
SHOW
swap(temp1, temp2);//std::swap(temp1,temp2);// 也可以自动识别
SHOW
getchar();
getchar();
return 0;
}
注意,这里的swap 没有指定名称空间。测试说明,这里的swap 调用的是,MyNamespace 中的swap 函数。
现在,任何地点的任何代码使用swap 置换两个T_OP 对象,C++ 的名称查找法则(name lookup rules)会找到T_OP 内的T_OP 专属版本。
如果想让这个class 专属版swap 在尽可能多的语境下被使用,需要同时在class 所在命名空间写一个non-member 版本以及一个std::swap 特化版本。
当然,上面的swap 其实,把它们放到了全局名称空间中也完全是ok 的。但,此时会将很多class、模板、函数、枚举等放到全局命名空间。
template<typename T>
void doSomething(T& obj1,T& obj2)
{
//...
swap(obj1,obj2);
//...
}
此时应该调用哪个swap?std版本?某个可能存在的特化版本?一个可能存在的T 专属版本且可能栖身于某个命名空间?希望的应该是调用T 的专属版本,且在找不到的时候调用std内的一般化版本:
template<typename T>
void doSomething(T& obj1,T& obj2)
{
using std::swap;// 令std::swap 在此函数内可用
//...
swap(obj1,obj2);// 为T 型对象找到最佳swap 版本
//...
}
- 一旦编译器看到对swap 的调用,查找顺序:
找到global 作用域或T 所在命名空间内的任何T 专属的swap。 - 如果T 是T_OP 并位于MyNamespace 内,编译器使用“实参取决之查找规则”找出WidgetStuff 内的swap。
- 如果没有T 专属之swap。使用std内的swap
如果已经存在std::swap 此时,编译器还是比较喜欢std::的T 专属特化办,而非一般的那个template,所以如果已经针对T 将std::swap 特化,特化版就会被编译器挑中:
上面这句话我不太理解,请看下面的例子
#include <iostream>
using namespace std;
#define SHOW cout << static_cast<void*>(temp1.getPsz()) << endl; \
cout << static_cast<void*>(temp2.getPsz()) << endl;
namespace MyNamespace {
template<typename T>
class T_OP
{
public:
T_OP() :psz(NULL) {}
T_OP(T* in_str) :psz(in_str) { }
~T_OP() { if (psz) { delete[] psz; psz = NULL; } }
void swap(T_OP & other)
{
//using std::swap; // 指示我们使用std中的swap,而且是CHAR_OP 独有的版本
std::swap(psz, other.psz); // 使用swap 的内部实现
}
T* getPsz() { return psz; }
T* setPsz(T* setPtr) { T* temp = psz; psz = setPtr; return temp; }
private:
T* psz;
};
template<typename T> // 特例化我们的swap 实现
void swap(T_OP<T>& a, T_OP<T>& b)
{
a.swap(b);//如果要置换CHAR_OP 调用其swap 函数
}
}
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap;
swap(obj1, obj2);
}
namespace std
{
template<>
void swap< MyNamespace::T_OP<char>>(MyNamespace::T_OP<char>& a, MyNamespace::T_OP<char>& b)
{
b.setPsz(a.setPsz(b.getPsz()));
}
}
int main()
{
MyNamespace::T_OP<char> temp1(new char[4]);
MyNamespace::T_OP<char> temp2(new char[4]);
SHOW
doSomething(temp1, temp2);//std::swap(temp1,temp2);// 也可以自动识别
SHOW
getchar();
getchar();
return 0;
}
实验得到,实际调用的是MyNamespace 内的swap。
当指定doSomething 中的swap 为 std::swap(obj1,obj2)会调用特化版的swap,没有MyNamespace 中的swap 函数时也会调用std 中的swap。
我更倾向于手动指定swap 的位置,这样更清晰
-----------------------------------------
总结,如果swap 的缺省实现对你的class 或 class template 提供可接受的效率,不需要做任何事情
否则:
- 提供一个public swap 成员函数,让它高效置换你的类型的两个对象值,它不应该抛出异常
- 在class 或 template 所在的命名空间中提供一个non-member swap,并令它调用上述swap 成员函数
- 如果你正编写一个class (而非class template),为你的class 特化std::swap,并令它调用你的swap 成员函数
如果你调用swap,确定包含一个using 声明式,以便让std::swap 在你的函数内曝光可见,然后不加任何namespace 修饰符,赤裸裸调用swap(还是不建议)。
swap 绝对不应该抛出异常。是这个技术的一个假设。因为高效的swap 几乎总是基于对内置类型的操作(比如指针),而内置类型上的操作绝不会抛出异常。
记住:
- 当std::swap 效率不高,提供自己的,且保证它不抛出异常
- 如果提供member swap,提供non-member swap 来调用它,对于class (非template),特化std::swap。
- 调用swap 时应针对std::swap 使用using 声明式,然后调用swap并且不带任何“命名空间资格修饰”
- 为“用户自定义类型”进行std templates 全特化是好的,但千万不要尝试在std 内加入某些对std 而言全新的东西。