正确重载 operator=
以一个整数数组类型为例, 下面的写法 class int_array { int* array; int size; public: int_array& operator=(int_array const& rhs) { if (this == &rhs) { return *this; }
delete[] array; array = new int[]; std::copy(rhs.array, rhs.array + rhs.size, array); size = rhs.size; return *this; }
/* other members */ };
是完全错误的. 设想在下面这样的上下文中 int_array a /* init */; int_array b; try { b = a; } catch (std::bad_alloc) { } /* ''b'' is bad now! */
进入 operator= 之后, 先执行了 delete[] , 这一句没问题, 如果之后的 array = new int[]; 内存分配失败, 这时 b 的成员 array 已经崩坏掉了, 以后再继续使用 b 显然是一件很糟糕的事情. 为了保持对象状态一致性, 很自然地应该将对象状态的切换放入一个尽可能安全的环境中. 一个方法是使用先复制一份对象, 然后利用绝对安全的 std::swap 将副本与当前对象交换. 假如在复制过程中出错, 并不会破坏当前对象的状态一致性. class int_array { public: int_array& operator=(int_array const& rhs) { if (this == &rhs) { return *this; }
int_array copy(rhs); swap(copy); return *this; }
void swap(int_array& other) { std::swap(array, other.array); std::swap(size, other.size); }
/* other members */ };
一个与之类似的例子是维护程序配置文件, 当程序在退出或者某个其它时机需要将配置写回文件时, 直接清空原来的文件然后提笔并不是一个好主意, 而应该先写入一个临时文件, 再将临时文件 mv 到配置文件.
Posted at Sep 30 2011 - 11:25:19
Permanent Link:
/p/430
Load full text
|
Post tags:
Operator Overload
C++
RAII
|
C++ 2011 中的 Left Value 与 Rvalue Reference
#include "某 C++ 概念之序篇" 答: std::stringstream() 产生的临时变量无法匹配类型 std::stringstream& . 但是 std::stringstream() 可以匹配类型 std::stringstream const& , 也就是说, 如果函数 std::stringstream& operator<<(std::stringstream& os, double d) 的签名和实现作修改成下面这个样子就行了 std::stringstream const& operator<<(std::stringstream const& os, double d) { const_cast<std::stringstream&>(os).operator<<(d); return os; }
之前扯过 C++ 已经没有语法意义上的左值了, 不过这里似乎有一点点很微妙的, 跟左值很类似的东西, 那就是, C++ 引用. 任何一本 C++ 入门书籍里面都会花一些篇幅来讨论引用, 其基本意义, 无非是 某个东西的别名, 因此, 这 某个东西对于引用而言显然是至关重要的. 最基本的一点是, 如果被引用的东西是临时变量 (顺便一提, 如果是字面常量这种比较尴尬的情况, 视同临时变量) 怎么办? 比如之前代码样例 A 中的情况? 这就牵扯到 C++ 中正统的 left value. 因为 left 已经不再有任何 "左" 相关的意义的, 所以我不太想在本文中把这货称之为 "左值" (实际上英文在这些方面更加灵活, 比如 Copyleft 这词跟 left 也没太大关系, 用来吐槽则非常不错). 一个东西如果是 left value, 那么它就可以被赋值给 left value 引用, 所谓 left value 引用嘛, 就是就是类似 std::stringstream& 的引用类型, 而暗含 const 的类型的引用则不算 typedef int const int_const; int_const& this_is_not_a_left_value_reference;
那究竟什么是 left value 呢? 一个语句之后仍然存在的变量, 表达式, 或者这样类似的 "具有 值属性的什么东西" 就是 left value. 而 std::cout << (std::stringstream() << d).str().length() << std::endl; 这句之后, 显然临时构造的 std::stringstream() 就被析构了, 因此这家伙不算是 left value, 自然也就无法被 std::stringstream& 所引用. 那常数为什么不算 left value 呢? 当然, 可以认为常数居阴阳之外, 别谈语句之后, 吐核退出, 就算是宇宙毁灭, 什么 π 啊 e 啊还活得好好的; 不过, 从计算机的角度来理解, 常数是 CPU 指令的立即数, 所以别谈语句之后, 只要这条指令结束, 它就没了. 然而, C++ 一个很邪恶的规定是, 即使一个东西不是 left value, 它仍然可以绑定 const 引用, 比如以下语句都是合法的 std::pair<int, int> const& pair = std::make_pair(0, 1); std::stringstream const& strm = std::stringstream(); std::string const& x = "Raki suta"; int const& i = 0;
这样直白地写出来是有点让人觉得不舒服, 不过作为函数调用的参数则好得多, 比如之前的代码片段 B 那样
Posted at Mar 28 2011 - 13:07:31
Permanent Link:
/p/232
Load full text
|
Post tags:
Move Semantic
Operator Overload
C++
Right Reference
Copy Constructor
Left Value
C++11
|
某 C++ 概念之序篇
C++ 虔诚的信徒们, 请阅读下面的代码, 回答码后的问题 /* 完整样例 A */ #include <iostream> #include <sstream>
std::stringstream& operator<<(std::stringstream& os, double d) { os.operator<<(d); return os; }
int main() { double d; std::cin >> d; std::cout << (std::stringstream() << d).str().length() << std::endl; return 0; }
问: 为什么上述代码无法通过编译? /* 代码片段 B */ #include <map> #include <string> #include <stdexcept>
struct key_error : public std::logic_error { explicit key_error(std::string const& what) : std::logic_error(what) {} };
std::string const& lookup(std::map<std::string, std::string> const& map , std::string const& key) throw(key_error) { std::map<std::string, std::string>::const_iterator i = map.find(key); if (map.end() == i) { throw key_error("key ``" + key + "'' not found in map"); } return i->second; }
问: 上述代码中的 lookup 函数有什么隐患? 答案已揭晓, 点此查看, 谢谢关注.
Posted at Mar 26 2011 - 09:37:59
Permanent Link:
/p/222
Load full text
|
Post tags:
C++11
Copy Constructor
Move Semantic
C++
Operator Overload
Left Value
Right Reference
|
C++ 到底怎么使用流来输出对象
书上写的做法当然是类似 struct my_type { void set_x(int xx); void set_y(double yy);
my_type(int xx, double yy) : x(xx) , y(yy) {}
friend std::ostream& operator<<(std::ostream& os, my_type const&); private: int x; double y; };
std::ostream& operator<<(std::ostream& os, my_type const& m) { return os << m.x << ' ' << m.y; }
一般使用这样没问题, 不过在想要写得很省略的用况下, 这样就很纠结 my_type m(10, 20.0); std::stringstream ss; std::string image = (ss << m).str(); // WRONG!
因为 operator<< 返回的类型已经变成没有 str() 取得字符串函数的基类 std::ostream& 了. 看起来要什么流类型进来什么流类型出才行, 比如 struct my_type { void set_x(int xx); void set_y(double yy);
my_type(int xx, double yy) : x(xx) , y(yy) {}
friend template<typename _OS> _OS& operator<<(_OS& os, my_type const&); // WRONG private: int x; double y; };
template <typename _OS> _OS& operator<<(_OS& os, my_type const& m) { os << m.x << ' ' << m.y; return os; }
不过很遗憾, 泛型函数无法做朋友 那要不先把 my_type 对象弄成字符串了, 再把字符串输出好了 struct my_type { void set_x(int xx); void set_y(double yy);
my_type(int xx, double yy) : x(xx) , y(yy) {}
std::string str() const { std::stringstream ss; ss << m.x << ' ' << m.y; return ss.str(); } private: int x; double y; };
template <typename _OS> _OS& operator<<(_OS& os, my_type const& m) // ACTUALLY NOT NEEDED ANY MORE { os << m.str(); return os; }
这看起来多疼啊. 一个可能的方案是, 为每个自定义类型实现 str() 成员函数 (有必要的还可以虚一下), 然后全局声明一个 template <typename _OS, typename _T> _OS& operator<<(_OS& os, _T const& t) { os << t.str(); return os; }
也许这样会好很多.
Posted at Mar 05 2011 - 15:14:50
Permanent Link:
/p/177
Load full text
|
Post tags:
C++
Operator Overload
Output Stream
|
C++ 中的左值
请使用 x86 32位 ArchLinux GCC 4.5.0 环境编译文中 C++ 示例代码, 不过只要是 GCC 或 clang++ 编译结果应该都相仿. 作为一个特别的语言, C++ 中的左值是*语义*概念而非*语法*概念, 这一点甚至也可以认为是沿袭自 C 语言. 在任何其他语言中讨论 “什么是左值” 这个概念时, 会简短使用 “变量可以是左值”, “表达式不能是左值”, “常数不能作为左值” 等等, 诸如 “变量”, “表达式”, “常数” 这些都是语法概念, 所以刚才这些说法都是左值的语法约定. 确实一些语言使用语法约束来规定左值, 比如 Python, 下面的 Python 代码 class A: pass
a = A() a + 1 = 0
编译时会报 “SyntaxError: can’t assign to operator”, 是 *Syntax* 哦. -甚至连- JS -这样乱的语言-也有类似的例子, 比如 var f = function() {}; f() = 0;
在执行的时候会报 "ReferenceError: Invalid left-hand side in assignment" --- Chromium. 在 C 里面表达式, 或者函数的返回值, 或者两者的混合, 只要语法恰当, 返回值类型又是一个可写指针, 那么通过对该指针引用的方式来赋值, 如 int* f() { return NULL; }
*(f() + 1) = 0;
这一特性在 C++ 那放荡不羁的类型系统里得到了极大的加强, 完全不再受制于语法的约束, 编译器只需要先根据运算符优先级搭起语法树, 至于左值判定什么的都留在以后的语义分析再来. 而这一切的开端仅仅是因为 C++ 支持一个指针的替代, 也就是引用, 这样连前缀星号都省略了. 另一方面, C++ 中的运算符重载机制又极大地扩展其左值概念的超凡脱俗, 并且 C++ 程序员似乎从来也未有过 “哦, 这只是个示例程序, 尽量别引入复杂特性” 这样的想法, 即使是经典的 Hello World 程序中, 也毫不吝惜地显摆着运算符重载这种高级特性. #include <iostream>
int main() { std::cout << "Hello, world\n"; }
从这个例子扩展出一个赋值语句几乎毫不费力, 只需要在函数体第一行加个 = 之后随手发挥就好. #include <iostream>
int main() { std::cout << "Hello World\n" = std::cerr; return 0; }
当然这份代码会报错但不是因为左值不合理, 而是 --- 赋值操作符被设为私有函数或赋值操作符重载被删除了 (C++11 中). 如果希望的话, 可以进一步参考下面这个操作符重载错乱的例子 (它可能由一位危险的 C++ 实习生在主键盘区 += 坏掉的机器上编写的) struct type { int x;
type& operator+(type& rhs) { this.x = rhs.x; return *this; }
type& operator=(type& rhs) { this.x += rhs.x; return *this; } };
int main() { type a, b, c; a + b = c; return 0; }
所以在 C++ 的世界里, 左值跟赋值运算操作符已经没有半点关系了, 倒是只要类型能够匹配上, 运算符重载就能通过编译, 看起来再离奇的算式都没问题. 不过, 如果非要扯赋值运算符, 它跟其它二元运算符有什么不同? 这差异还确实有, 那就是, 赋值运算符必须是成员非静态的, 也就是说下面的编译无法通过
Posted at Jun 19 2010 - 13:05:49
Permanent Link:
/p/85
Load full text
|
Post tags:
C++
Left Value
Operator Overload
|