About

`std::function` 基本实现

Permanent Link: /p/525/

Post tags:

C++

C++11

STL

C++ Lambda 简易上手

    本文所有代码示例通过 g++ 4.6 编译.

    之前有吐槽过 C++ STL 算法中的 for_each 函数华而不实, 主要原因是受限于 C++ 的函数编写语法, 即传统上 C++ 要求函数须独立编写, 而不能嵌套在其它函数内. 不过, 欢迎来到二十一世纪的第一个 0x10 年, 现在有了 lambda.
    与其它语言一样, C++ 的 lambda 就是个匿名函数, 它的语法结构如
[ 上下文变量绑定方式 ] ( 形式参数列表 ) 可省略的返回值类型声明
{
    函数体
}
    匿名函数可以定义在任何地方, 作为一个表达式出现; 如果在匿名函数的函数体中使用了表达式所在上下文中定义的量, 则需要指定绑定方式. 如
// 定义变量 sqr 为一个匿名函数
// 这个匿名函数的上下文变量绑定方式为空, 因为它无须引入任何上下文变量
// 这个匿名函数接受一个 int 作为参数
// 返回值类型声明省略, 由编译器推倒出返回 int
auto sqr = [](int x)
           {
               return x * x;
           };

int x = 10;
// 这个匿名函数的上下文变量绑定方式为 & 即引用方式, 它引用了上文中定义的 x
// 这个匿名函数接受一个 int 作为参数
// 返回值类型声明为 bool
auto equalsToX = [&](int y) -> bool
                 {
                     return x == y;
                 };

// 这个匿名函数的上下文变量绑定方式为 = 即复制方式, 它引用了上文中定义的 x
// 这个匿名函数接受一个 int 作为参数
// 返回值类型声明省略
auto lessThanX = [=](int y)
                 {
                     return y < x;
                 };
    可见多亏了 C++ 的强制类型声明和变量定义检查, 才把一个简单的匿名函数搞得这么复杂, 解决了不少其它语言中不存在的问题.

    作为 C++ 程序员的一个特异能力便是能够在写代码时就预见到这段程序运行时内存与对象的流动. 看了上面匿名函数的各种奇葩绑定上下文的方法, 很明显它们不仅是一个函数那么简单, 如果必要匿名函数会占用一些内存空间, 可以做这样一个实验来试试.
struct Point {
    Point()
        : x(0)
        , y(0)
    {}

    double x;
    double y;
};

int main()
{
    Point p;
    auto f = [&]()
             {
                 std::cout << p.x << "," << p.y << std::endl;
             };
    auto g = [=]()
             {
                 std::cout << p.x << "," << p.y << std::endl;
             };
    std::cout << sizeof(f) << " - " << sizeof(Point*) << std::endl;
    std::cout << sizeof(g) << " - " << sizeof(Point) << std::endl;

    auto sqr = [](int x)
               {
                   return x * x;
               };
    std::cout << sizeof(sqr) << std::endl;

    return 0;
}

Permanent Link: /p/505/

Post tags:

C++

C++11

Labmda

Nested Function

在 C++ 模板类体中将类名仍视为模板

    用 Clang++ 编译下面的源代码 (已测试 ArchLinux clang 3.2-3)
template
 class T>
class A {};

template
class B {
    A member;
};

int main()
{
    B b;
    return 0;
}
会得到如下错误信息
error: template argument for template template parameter must be a class template
    A member;
      ^
也就是说, clang++ 会认为出现在模板类 B 的类体中的符号 B 是一个已经实例化过的类型, 不能够被传递给模板类 A 用于实例化.
    而 g++ 4.7 可以正确编译通过, 说明 g++ 肯定内置了某种脑补机制, 能灵活处理这样的情况, 它甚至可以编译如下分裂的代码
template  class T>
class A {};

template
class C {};

template
class B {
    A member;
    C another;
};

int main()
{
    B b;
    return 0;
}
    当然在 clang++ 下也不是没办法解决了, 方案是, 在类体中如果要将类名当作模板来使用, 则在名字前加上名字空间声明. 如上面 B 定义在全局名字空间中, 所以代码应该这样修改
template  class T>
class A {};

template
class B {
    A< ::B> member;
};

int main()
{
    B b;
    return 0;
}
    话说小于号跟双冒号之间有个空格 < ::, 这绝不是我手残了抖上去的, 而是这地方必须要一个空格. C++ 程序设计领域流行着各种都市传说, 其中有模板套模板结束时的两个大于号之间要加个空格什么的, 类似, 小于号跟冒号之间也要加个空格, 否则 <: 会被认为是 [.
    相应地, 如果 B 定义于某个有名字的名字空间 (这话说得...) 中那么上面代码应作相应修改
template  class T>
class A {};

namespace ns {
    template
    class B {
        A<ns::B> member;
    };
}

int main()
{
    ns::B b;
    return 0;
}

Permanent Link: /p/503/

Post tags:

C++

Template

句读探析 - 又一个 Javascript 生成器

    前段时间博客没怎么更新, 因为不才去刷一个编译器项目了. 这项目是个 Javascript 产生器.
    起这个念头的契机是看到连 M$ 都动心思搞 JS 生成器了 (然而不得不吐槽的是, 这语言看起来跟 JS 简直一个德行). 而模仿的对象则是 CoffeeScript, 可是我又不想去分支 CoffeeScript 改点语法什么的改成 LatteScript 或 CappuccinoScript (我对 Coffee 的某些语法不很满意), 于是乎自己折腾了一个.
    不过另一方面, 这个编译器 (暂定名是 Stekinscript) 也不是完全无中生有, 而是从我的毕业项目修改过来的. 要说这样有什么不好的话, 就是 --- 这东西用 C++ 实现的 (准确地说是 C++11); 要说有什么好的话, 就是貌似这样可以直接集成 v8, 嗯 (自黑一下, 我没考察过这个, 也没有做这个事情的计划).

    下面介绍一下这个语言本身, 撇开日常的什么分支语句啦函数定义返回这些 (详情可参考文档), 先说说重点.
    首先 Stekinscript 支持缩进语法, 类似 Python 跟 CoffeeScript. 代码很重要的一点就是要看着舒服不是么?
    其次就是非强制类型, 强制类型给人的感觉呢, 就像三岁小孩子学写字旁边有个执戒尺的先生一样, 错一点就打手, 我们已经是断奶的程序员了好么, 所以什么 Dart 就先搁下不用了.
    最后是实际使用的感觉, 这一点我排斥 CoffeeScript 的 lambda 语法如 (x)-> x * x 这种. 要知道输入类似 -> 这样的符号两个字符都在右手无名指跟小指区, 还是一上一下, 而且减号不用按住 Shift 而大于号又需要, 这是有多别扭设计语言的人没知觉么? 还是说已经练成星际争霸九段这点微操不算什么?

    吐槽就到此为止, 下面贴个 Stekinscript 的例子
x: []
setTimeout(():
    if x.length = 10
        return
    x.push(document.getElementById('input').value)
, 2000)

Permanent Link: /p/497/

Post tags:

C++

Compiler Construction

Stekin

多态泛型不两全 - 面向对象中的协变与逆变

在面向对象语言中, 子类覆盖父类中的实例成员函数时, 可以酌情修改返回值类型, 使之变得更加具体. 比如在 C++ 中, 下面的代码是可行的.

Code Snippet 0-0

class base {
public:
   virtual base* copy() const
   {
       return new base;
   }
};

class inherit: public base {
   virtual inherit* copy() const
   {
       return new inherit;
   }
};

因为 inherit* 类型可以无压力转换为 base* 类型, 所以这样做并不会破坏多态机制. 这种做法称之为协变 (covariance).

然而协变一方面满足着面向对象设计的种种需求时, 另一方面与泛型的协作却非常糟糕, 比如如果尝试使用自动指针取代上述代码中的返回值类型就不正确了

Code Snippet 0-1

class base {
public:
   virtual std::unique_ptr

copy() const
   {
       return new base;
   }
};

class inherit: public base {
   virtual std::unique_ptr
copy() const
   {
       return new inherit;
   }
};

编译这一段代码会得到类似如下的错误

invalid covariant return type for 'virtual std::unique_ptr inherit::copy() const'

简而言之, std::unique_ptrstd::unique_ptr 并无实质上的继承关系, 因此无法如此重定义子类函数的返回类型.

这并不是语言层面出现的疵漏, 如果这确实可行, 反而还会引起问题, 举个例子

Code Snippet 0-2

class fruit {};
class apple: public fruit {public: void do_something_as_apple();};
class banana: public fruit {public: void do_something_as_banana();};

int main(void)
{
   std::unique_ptr a(new apple);

   /* 如果子类的自动指针对象是可以为父类的自动指针的引用直接引用
      也就是说下面这一句合法 */
   std::unique_ptr& f = a;

   /* 接下来利用 f 来重置内部的裸指针 */
   f.reset(new banana);

   /* 由于 f 就是 a 的引用, 因此 a 中的裸指针指向了
      与 apple 类型不兼容的 banana 的实例 */
   a->do_something_as_apple(); // 謎の結果
   return 0;
}

因此, 这种利用自动指针绕着弯子来造错误的行为是绝对不被允许的. 此外, 下面的这种赋值方式 (虽然明显看起来很怪异) 也是不被允许的

int main(void)
{
   inherit* i;
   base*& b = i; // 跪
   return 0;
}

之前的一篇文章也有简单介绍到, 不过当时说的是父类的二级指针不能被子类二级指针所赋值.

除了指针之外, 可以想象数组, 包括 STL 容器也会纷纷中枪, 比如 std::vector 类型的引用是无法引用 std::vector 类型的对象的.

在 Java 语言中, 为了缓解这个问题, 引入了泛型通配符. 这个名字听起来就很语死早, 又是泛又是通配, 不过程序设计世界就是这么充满纠结. 下面是 Java 中的解决方案

Permanent Link: /p/490/

Post tags:

C++

Java

面向对象程序设计

万能巧械 - 二叉检索树 [壹]

二叉检索树的查询与检索操作看这里

    从二叉检索树中删除一个元素分多个步骤:
  • 找到节点
  • 将节点移动到容易删除的位置
  • 删除节点
    找到节点与查询操作非常类似, 只不过, 正如插入元素过程中需要将路径上所有节点负载加上 1, 删除过程中, 也需要将节点路径上所有节点负载都减去 1.
    下面是这一部分的实现
void remove_by(key_type const& key)
{
    remove_by_from(key, &root);
}

void remove_at(int position)
{
    remove_at_from(position, &root);
}

static void remove_by_from(key_type const& key, node** parent)
{
    --((*parent)->load);
    if (key < (*parent)->t.key()) {
        remove_by_from(key, &((*parent)->left));
        return;
    }
    if ((*parent)->t.key() < key) {
        remove_by_from(key, &((*parent)->right));
        return;
    }
    remove(parent);
}

static void remove_at_from(int position, node** parent)
{
    --((*parent)->load);
    int left_load = (NULL == (*parent)->left) ? 0 : (*parent)->left->load;
    if (position < left_load) {
        remove_at_from(position, &((*parent)->left));
        return;
    }
    if (left_load < position) {
        position = position - left_load - 1;
        remove_at_from(position, &((*parent)->right));
        return;
    }
    remove(parent);
}

static void remove(node** n);
    这些函数都是 class binary_search_tree 旗下的. 与插入操作类似地, 这里需要的同样是节点的二级指针, 因为当节点从树中被移除时, 必然伴随着对其父节点对应的子树指针修改.

    找到了节点之后将会有下面 3 种情况
  • 节点是叶子
  • 节点只有一侧子树
  • 节点两侧子树健全
    前两种情况好办, 找到为空的一侧子树, 将另一侧子树接到父节点上, 然后删除当前节点即可. 也就是说当前已经处在容易删除的位置, 可以跳过一个中间步骤. (P 表示待删除节点的父节点)

Permanent Link: /p/481/

Post tags:

Algorithm

Binary Search Tree

B-tree

C++

Data Structure

Generic Programming

Order Statistic

Template

万能巧械 - 二叉检索树 [零]

    在维护固定的顺序统计量有先天优势, 但在面对如下需求的时候又极其手短
  • 按键索引 - 给定一个元素的键, 在数据集合中找到与该键相同 (或较小, 较大) 的元素
  • 随机索引 - 在堆建立时, 可以给定一个固定的数值 K, 让堆维护第 K 大的元素, 但毕竟这个 K 是建堆时固定的
  • 多键索引 - 堆中的元素的排序索引是唯一的, 如果想要其它的索引, 则需要借助额外的数据结构
    前两个手短指的是时间复杂度, 要完成这些功能, 跟直接到未排序数组里面去暴力解决是一回事. 而后面一个则是堆的硬伤. 严格来说, 堆并不是一种数据结构, 说穿了堆只有数据而没有结构 (是的, 即使是数组也是数据结构, 因此严格来讲应该说成, 它没有自身独特的结构, 还记得 STL 中优先队列的模板定义吧, 它的底层存储结构是可以通过模板参数替换的), 其精髓在于算法. 由于不具备结构, 也就是说没有实质上的数据与数据的关系, 因而不方便弄出多个不同的键. 这个现在空谈就像白切鸡一样没什么味道, 以后有机会再加上油盐详述.

    较之堆更加全能的索引数据结构非二叉检索树 (binary search tree, 缩写为 BST) 莫属了, 一般的二叉检索树可以很轻松地解决按键索引, 而若将子树节点计数器加诸其上, 就能进行快速的随机索引. 不过, 在这篇文章中, 只讨论理想状态下的二叉树, 不考虑那种被精心准备的数据叉成链表的情况.

    首先还是把二叉树的架子给搭出来
template

class binary_search_tree {
    struct node {
        T t;
        node* left;
        node* right;
        int load;

        explicit node(T const& rhs)
            : t(rhs)
            , left(NULL)
            , right(NULL)
            , load(1)
        {}
    };

    node* root;

    typedef typename T::key_type key_type;
public:
    binary_search_tree()
        : root(NULL)
    {}
public:
    void insert(T const& e);

    void remove_by(typename T::key_type const& key);
    void remove_at(int position);

    T& element_by(typename T::key_type const& key);
    T& element_at(int position);
};

Permanent Link: /p/479/

Post tags:

Algorithm

Binary Search Tree

C++

Data Structure

Generic Programming

Order Statistic

Template

日常的数据结构 - 堆的实现与第 K 最小堆

    在前一篇文章中纸上谈了堆的性质以及如何在插入元素和弹出最值时保持这些性质. 这篇文章将聊聊实现方式.
    从实现的角度来说, 使用完全二叉树作为堆的前提的好处是, 完全二叉树非常容易实现, 甚至可以说是最容易实现的二叉树. 由于完全二叉树的节点编号是连续的, 那么它可以被拉平, 放进一个日常的数组中, 如

       +---+
       | 4 |
       +---+
      /     \
   +---+   +---+
   | 5 |   | 9 |
   +---+   +---+
  /     \
+---+   +---+
| 8 |   | 5 |
+---+   +---+

    这样一棵完全二叉树可以被转换成

      .----.
     /--.   \
+---+---+---+---+---+---+-----
| - | 4 | 5 | 9 | 8 | 5 | ...
+---+---+---+---+---+---+-----
         \______/   /
          \________/

    其中的线连接节点与它们的子节点. 如果用节点的编号来标识这个数组, 则会是

      .----.
     /--.   \
+---+---+---+---+---+---+-----
| 0 | 1 | 2 | 3 | 4 | 5 | ...
+---+---+---+---+---+---+-----
         \______/   /
          \________/

    这里有个很奇妙的性质, 索引为 i 的节点, 它的左子节点的索引是 2i, 而右子节点的索引是 2i+1, 其父节点索引则是 floor(i/2) (根节点除外). 如果用 0 号节点而不是 1 号节点存储根节点呢? 如

      .----.
     /--.   \
+---+---+---+---+---+---+-----
| - | 0 | 1 | 2 | 3 | 4 | ...
+---+---+---+---+---+---+-----
         \______/   /
          \________/

    也能很容易计算, 索引为 i 的节点, 左子节点索引是 2i+1, 右子节点索引是 2i+2, 父节点索引是 floor((i-1)/2). 似乎也没什么太大区别. 不过, 之前那种计算方式的好处在于, 2i, 2i+1, i/2 这样的算式都能换成极快的位运算: 2i 等效于 i << 1, 2i+1 等效于 (i << 1) | 1, floor(i/2) 等效于 i >> 1, 这还能提供一丁点效率优化 (和一部分代码混乱程度加成, 以及大量的极客自豪感上升).
    既然堆的逻辑结构是数组, 那么可以采用 std::vector 作为存储数据结构. 此外, 将比较方式以模板形式抽出, 这样可以构造一个抽象的最值堆, 而不是死板的最大堆或者最小堆. 下面是堆的框架
template

class heap {
    std::vector<_T> array;
    _Less const less;
    typedef typename std::vector<_T>::size_type size_type;
public:
    heap()
        : array(1) /* insert a placeholder, array[0] */
        , less(_Less())
    {}

    void push(_T const& value);
    _T pop();
};
    向堆中加入元素实现为插入并修正, 修正的过程, 也就是向父节点方向移动, 是递归实现的

Permanent Link: /p/441/

Post tags:

Algorithm

C++

Data Structure

Generic Programming

Heap

Order Statistic

Template

对空结构体求 sizeof

C++ 声称完全兼容了 C, 这一点在某些细节上不尽然. 比如对空结构体 --- 没有成员, 不含虚函数, 虽然 C 还生活在没有虚函数的三叠纪 --- 求 sizeof 的结果. 具体地说就是下面这个表达式

Code Snippet 0-0

struct empty {};

sizeof empty; // 值为 0

在 C 和 C++ 中会得到不同的值: C 中其值为 0 (在主流编译器中如此), 而 C++ 中其值为 1. 这个微妙的不同步源于 C 中的一个指针相减问题, 如下代码

Code Snippet 0-1

#include


struct empty {};

int main()
{
   struct empty x;
   struct empty y;
   printf("%ld", &x - &y);
   return 0;
}

以 C 语言编译并运行, 程序会直接崩溃掉, 因为在 C 中计算表达式 &x - &y 的值等同于 ((char*)&x) - ((char*)&y) / sizeof struct empty. 这个整数除法非常糟糕, 毫无疑问 C 编译器应该了解到危险所在了: 在编译期, 它完全能够发现该除法算式的常数分母是整数 0, 但是它还是义无反顾地生成了代码, 甚至连警告也不给, 将程序推入运行时崩溃的深渊.

本来这种事情应该偷偷改掉拉倒, 可是 C 标准对这个事情讳而不谈, 丢出一张王牌 "对空结构体或联合求 sizeof 将会是*未定义*行为". 对此 C++ 只好吐了个槽, 说*任何对象至少要占用 1 字节空间*. 所以其实 C++ 标准也没有明确说出 "对空结构体或联合求 sizeof 将会是 1" 这样的话, 但是根据前面这个规定, 由编译器厂商演绎出来的结果就是这样的, sizeof 纷纷得到结果 1, 包括下面这样的情况

Code Snippet 0-2

struct empty_base_a {};
struct empty_base_b {};
struct empty_inherit : empty_base_a, empty_base_b {};

sizeof empty_inherit; // 值为 1

即对从空类上 (多重) 继承的空子类求 sizeof 也将得到 1.

这一招看起来很挫, 但还真的管用了, 用 C++ 编译器编译并运行上述程序, 零也不除了, 程序也不会崩了, 还能给出正确地结果.

虽然把两个什么空的东西用继承的方式捏在一起不会产生体积变大, 但是一个数组的什么空的东西则会导致体积累加, 如

struct empty {};

int main()
{
   struct empty x[4];
   printf("%ld", sizeof x); /* 4 */
   return 0;
}

这段 C++ 代码的运行结果将是 4, 也就是 x 占用了 4 个 1 字节. 这又扯到 C++ 另一个核心编程思维 --- 面向迭代器. 例如下面一坨代码

Code Snippet 0-3

struct empty {};

void echo(empty)
{
   std::cout << "echo" << std::endl;
}

int main()
{
   struct empty x[4];
   std::for_each(x, x + 4, echo);
   return 0;
}

如果认为整个数组是一个对象, 打个包求 sizeof 才能得到 1, 而 x[0]x[4] 等等有相同的地址, 那么 std::for_each 中的循环将一次也不被执行. 类似的, 让多个空类对象聚合在一个空类对象中时, 它们占用的空间大小是会累加的, 如

Code Snippet 0-4

struct empty {};
struct twin {
   empty a;
   empty b;
};

sizeof twin; // 2

Permanent Link: /p/438/

Post tags:

C

C++

sizeof

正确重载 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 到配置文件.

Permanent Link: /p/430/

Post tags:

C++

Operator Overload

RAII

日常的算法 - 顺序统计

    顺序统计指的是在一坨并没有排序的数据上找出跟顺序量有关的元素. 典型的顺序统计包括找最小值或最大值算法, 这两个算法可以说没有任何技巧而言, 暴力地遍历一次数据集合就行了, 如找最小值算法的实现 (以 C++ 面向迭代器的程序设计描述, 不考虑集合为空的情形. 以后的例子相同)
template

_Iterator find_min(_Iterator begin, _Iterator end)
{
    _Iterator min = begin;
    for (++begin; begin != end; ++begin) {
        if (*begin < *min) {
            min = begin;
        }
    }
    return min;
}
    将这两件事情揉合在一起, 即从一个集合中同时找到最小值和最大值, 相比于分头寻找, 能够节省不少时间, 原因不仅仅是两次循环合并成一次循环, 运用一些技巧能显著地减少比较的次数.
    在每一次取出剩余元素时, 同时取出两个, 先将这两个比较大小, 然后将较小的元素与当前最小值比较, 而将较大的值与当前最大值比较取出

   +-------+-----------+-----
   | begin | begin + 1 | ...
   +-------+-----------+-----
       |         |
       +--( < )--+
           / \
          /   \
+----------+   +-------------+         +-----+
| less one |   | greater one |--( < )--| max |
+----------+   +-------------+         +-----+
     |
     |                                +-----+
     +-------------------------( < )--| min |
                                      +-----+

    这样每寻访 2 个元素, 需要 3 次元素比较, 加上判断循环是否结束需要 1 次比较, 而分开查找, 则每 1 个元素需要 2 次元素比较, 加上 1 次循环判断结束的比较. 之所以这么计较比较的次数, 因为目前计算机体系结构和分支语句非常不友好, 太多分支 (意味着大量的预测失败) 的程序会因为无法充分利用流水线而显著地降低实际执行效率.
    下面是实现的例子

Permanent Link: /p/426/

Post tags:

Algorithm

C++

Generic Programming

Order Statistic

STL

Template

头文件禁区: C++ 中的 ABI

    在上一篇文章中写到了 C 的 ABI (application binary interface) 有关的内容. 而 C++ 这货也采用 header / cpp 分离的方式来组织代码, 并在编译时将头文件直接在 cpp 中插入, 这种暴力方法已经不能简单说是为了兼容 C 的各种编译期行为, 完全就是彻头彻尾地重蹈覆辙, 甚至有过之而无不及.

    首先, C++ 也会遇到 "如果直接用了动态库中修改过签名的函数怎么办" 这个全静态语言的千古难题. 而且比起 C 语言更要命的是, C++ 的类是不能进行前置声明来回避对象内存布局耦合: 前置声明就没有引用成员函数了, 那还怎么面向坑爹的对象呢? 不仅仅对象成员布局有此问题, 连同 C++ 藉由实现多态的虚函数亦面临着同样的窘境. 试想动态库中虚函数表都改得面目全非时, 使用该对象的代码却还用原来的方式访问, 那还不是各种鸡飞狗跳.
    对于如此恶劣环境下仍然想保持 ABI 兼容性的 C++ 程序员来说, 倒也不是完全无路可走. 具体的方案就不在这篇文章中赘述了, 请转到陈硕老师的 C++ 工程实践: 二进制兼容性C++ 工程实践: 避免使用虚函数作为库的接口. 两篇文章都写得非常详细.

    撇开这些跟 C 语言有关的孽缘不谈, C++ 本身也提供了一些机制来帮助程序员养成编码恶习, 比如一些函数实现可以直接写进头文件中, 而无须担忧任何链接错误. 首当这个批评的就是成员函数, 包括静态或非静态, 虚或非虚. inline 函数或者 template 函数实现要写头文件这点可以理解可以忍, 但成员函数来凑什么热闹? 成员函数的不往头文件里面写, 可以避免一些 ABI 耦合, 比如
class Date {
    int month;
    int day;
public:
    int getDay() const;
    int getMonth() const;
};
    这种情况下, 在使用
Date* date /* initialize */;
date->getDay();
不必担心内存布局耦合, 因为作为非虚的成员函数, Date::getDay() const 这样的函数签名正如 getday(Date const*) 一样, 这就回到了上一篇文章最后提到的解决方法了. 但是如果用得不恰当, 比如头脑发热认为 inline 一下能提高效率什么的, 而活生生地写成
class Date {
    int month;
    int day;
public:
    inline int getDay() const
    {
        return day;
    }

    inline int getMonth() const
    {
        return month;
    }
};

Permanent Link: /p/404/

Post tags:

ABI

C++

inline

STL

Template

C++ inline 坑爹真相

    准备两个文件 a.cpp b.cpp, 内容如下
/* a.cpp */

#include


void b();

inline void echo()
{
    std::cout << 0 << std::endl;
}

int main()
{
    echo();
    b();
    return 0;
}

/* b.cpp */

#include

inline void echo()
{
    std::cout << 1 << std::endl;
}

void b()
{
    echo();
}
    然后执行
$ c++ a.cpp b.cpp -o a.out && ./a.out
    也许会出现两个 0, 或者两个 1, 但是不会出现 0 1 这样的组合, 更不会链接冲突了. 如果有任何跳过生成目标文件直接到生成可执行文件的顾虑, 尝试下面的方法结果是一样的
$ c++ -c a.cpp
$ c++ -c b.cpp
$ c++ -o a.out a.o b.o
$ ./a.out
    甚至最直接地, 如果查看两个中间文件的符号
$ nm a.o
$ nm b.o
    会在两次结果中都看到一条名为 _Z4echov 的记录, 这就是 echo 函数被 name-mangling 之后的标识.

    C++ 文档和标准中过度赞扬 inline 这个关键字, 然而一笔带过的是, 编译器可以选择忽略 inline 标记, 至于忽略了会有什么后果, 如何链接等细节都一字不提. 如果这是一个封装行非常良好的编译器特性, 那也就没什么. 但实际上这里敷衍了一些重要事实, 既然 0) 有的函数即使程序员写了 inline, 编译器可以不进行 inline, 则 1) 很有可能出现这样的函数, 并在编译后被生成到目标文件中去, 而且 2) 由于 inline 函数一般实现在头文件中, 那么 3) 编译器在编译每一个包含了这个头文件的 cpp 文件时, 都会将该函数符号生成到对应目标文件中去, 而若要 4) 链接这多个目标文件不发生链接冲突, 只好由 5) 编译器或者链接器耍一些手段来保证, 可是 6) 耍了手段, 却有可能造成上面的问题发生.

    与 inline 失败的函数有关的真相是, 编译器会为 inline 函数做上标记, 链接器根据这个标记, 从目标文件中找到这个函数的多处实现, 不负任何责任地选取其中一个作为正身, 并抛弃其它的实现, 无论这些实现是否相同 (链接器也无从判断是否相同).
    开头的例子非常猎奇, 但是也不排除这样日常的情况: 0) lib 作者因为各种原因, 让 inline 失败的函数编入 lib; 1) 这个函数没有在文档中被提及, 头文件也隐藏得很好; 2) 引用 lib 的程序员定义了一个同 namespace 且同名的 inline 函数 (当然最有可能在全局名字空间这么干了). 于是就悲催了.

    现在再回头看一下 inline 的描述
  • 要想 inline, 得先 inline
  • 若是 inline, 未必 inline
    这是不是有种在看葵花宝典开头的感觉?

Permanent Link: /p/379/

Post tags:

C++

inline

C++ 对象构造时的异常与 RAII 的救赎

    在上一篇文章中简单介绍了一下 RAII 的原理, 举了个读文件的例子. 这个例子稍微单薄了一些, 它只封装了一个需要 RAII 机制管理的资源 (FILE*). 软件工程中流行的观念是, 不具备扩展性, 经不起追加功能的东西注定会悲剧. 现在假如需要给这货增加个缓冲区, 也许这样是可以的
struct File {
    File(char const* filename, int buffer_size)
        : file(fopen(filename, "r"))
        , buffer(new char[buffer_size])
    {
        if (NULL == file) {
            throw std::runtime_error(std::string("fail to open ") + filename);
        }
    }

    ~File()
    {
        fclose(file);
        delete[] buffer;
    }
private:
    FILE* file;
    char* buffer;

/* other members */
};
    在 buffer 分配失败时, 一般会抛出 std::bad_alloc.
    这个类型的破绽相当多, 稍不注意就有可能漏资源. 首先是刚刚提到的 buffer 分配失败抛异常, 那么假如这个时候 file 已经打开成功了, 它会被关闭么? 其次, 假设 buffer 成功分配, 但这时 file 打开失败, 那么 buffer 是否会被释放呢?
    很不幸的, 两者的答案都是. 还是那句话, 因为 File 的构造函数没有走完, 这时抛出异常, 那么析构函数不会被执行. 因此, 不要尝试构造控制多于一个资源的类型. 而遇到这种需求, 应该拆分资源, 然后将这些单元类型进行聚合, 如
struct File {
    explicit File(char const* filename)
        : file(fopen(filename, "r"))
    {
        if (NULL == file) {
            throw std::runtime_error(std::string("fail to open ") + filename);
        }
    }

    ~File()
    {
        fclose(file);
    }
private:
    FILE* file;

/* other members */
};

struct Buffer {
    explicit Buffer(int buffer_size)
        : buffer(new char[buffer_size])
    {}

    ~Buffer()
    {
        delete[] buffer;
    }
private:
    char* buffer;

/* other members */
};

struct BufferedFile {
    BufferedFile(char const* filename, int buffer_size)
        : file(filename)
        , buffer(buffer_size)
    {}

    File file;
    Buffer buffer;

/* other members */
};

Permanent Link: /p/363/

Post tags:

C++

Exception Handling

RAII

从 Python 的 with 到 RAII

    在上一篇文章中提到了 Python 的 with 结构, 其背后的思想正是 RAII. 在 C++ 中, RAII 的样子看起来跟 Python 中的会非常不一样, 还是以打开读取文件为例子
#include

#include
#include

struct File {
    explicit File(char const* filename)
        : f(fopen(filename, "r"))
    {
        if (NULL == f) {
            throw std::runtime_error(std::string("fail to open ") + filename);
        }
    }

    std::string readLine()
    {
        char buffer[256] = { 0 }; // just for demo
        if (NULL == fgets(buffer, 256, f)) {
            if (!feof(f)) {
                throw std::runtime_error("error occurs while reading file");
            }
            throw std::out_of_range("end of file")
        }
        return buffer;
    }

    ~File()
    {
        fclose(f);
    }
private:
    FILE* const f;
};

int main()
{
    try {
        File file("litsu");
        while (true) {
            std::cout << file.readLine();
        }
    } catch (std::runtime_error e) {
        std::cerr << e.what() << std::endl;
    } catch (std::out_of_range) {}
    return 0;
}
    注意看 try 块中间的语句, 看起来流程很清晰, 完全不像 Python 里面那样, 还要弄个 with 块, 多一级缩进. 都说 C++ 代码没 Python 的精神, 然而在这种小地方 C++ 反而超过了 Python 呢.

    其实, Python 的 with 块更像个语法糖, 上篇文章中有提到, 双层 try 在 Python 中能等效地解决这个问题, 只是看起来丑很多. 这个问题的根本在于, Python 这样不具备 RAII 特性的语言没能处理好对象从不可用状态切换到可用状态的状态过渡. 回顾一下双层 try 结构的处理方式
def readFile():
    try:
        f = open('sawako', 'r')
        pass
        try:
            process(f.readlines())
        except:
            print 'error occurs while reading file'
        finally:
            f.close()
    except:
        print 'error occurs while reading file'

Permanent Link: /p/358/

Post tags:

C++

Exception Handling

Python

RAII

Python 中函数交叉引用中的问题与解决方法

    Python (下面的讨论都是基于 2.7 版本的) 中, 先定义的函数可以调用后定义的函数, 比如下面这段代码
def first():
    return second()

def second():
    return 0

x = first()
    不过, 这种调用是有局限性的, 稍作修改为下面这个样子
def first():
    return second()

x = first()

def second():
    return 0
就会产生符号错误, 解释器给出的信息是符号 second 不存在.

    Python 作为一门解释型语言, 出现这样的事情很有可能是这样的原因, 假设现在正在交互模式下执行, 那么输入
def first():
    return second()

x = first()
到此为止, second 这个符号确实还没有被定义, 故产生错误. 但是如果上述代码内容是写在文件里的, 那么 Python 完全可以扫描整个文件以获得充分的上下文信息, 并正确地检测出 second 函数定义, 只是 Python 没有这么做.

    熟悉 C++ 的同学这时候肯定跳出来了, 因为在 C++ 类空间中, 这种引用是合法的, 如
struct type {
    int first()
    {
        return second();
    }

    type()
        : x(first())
    {}

    int second()
    {
        return 0;
    }

    int x;
};
不过, C++ 这种事情其实极其危险的, 比如下面这一段代码
struct type {
    int first()
    {
        return second();
    }

    type()
        : x(first())
    {}

    int second()
    {
        return x;
    }

    int x;
};
这实际上使用了还处于量子态的 x 来初始化它自身, 虽然这样的代码只有手贱的程序员在极其罕见的情况下才会写出这样的代码 (比如我在写这篇博客的时候), 但是本着编译器的职责, 完全应该想方设法让这种情况编不过去.

    好了, 还是先回到 Python 上来, Python 不允许这样的代码编译过去, 是因为这个原因么? 假设有下面的程序成功被解释执行
def first():
    return second()

x = first()

def second():
    return x

Permanent Link: /p/314/

Post tags:

C++

Compiler Construction

Python

C++ 2011 中调用对象的 Rvalue Reference 成员函数重载

    之前提到的 rvalue reference 还有一点没有结算, 其实本来早就想写完它, 但是尝试了很久, 最后到 stackoverflow 上问来问去才最终解决, 那就是当某个对象是 rvalue 时, 怎么调用 rvalue 的成员函数重载.
    之前在有关 rvalue 函数重载的讨论中说到参数是 rvalue 的情况, 很自然地, 如果认为 (简单起见, 非 virtual 的) 对象成员函数等价于普通函数加上一个 this 参数, 那么如何分辨 this 参数是 left value 还是 rvalue 呢? 受到对象成员函数可以根据 const 修饰与否来重载这个现状的启发, 对于对象本身是 rvalue 的重载当然很可能是为成员函数加上某种修饰, 类似下面这个样子
struct Type {
    void func();                     // for normal left reference
    void func() const;               // for const left reference
    void func() rvalue-qualified-id; // for rvalue reference
};
    想法当然好, 而且确实有这么一回事, 只是标准敢正常一点么? 下面上真实代码, 这段代码目前似乎只能在 clang++ (我试验的版本是 2.9) 上编译通过
#include


struct Type {
    Type() {}

    void func() &
    {
        puts("a");
    }

    void func() const&
    {
        puts("b");
    }

    void func() &&
    {
        puts("c");
    }
};

int main()
{
    Type a;
    a.func();

    Type const b;
    b.func();

    Type().func();

    return 0;
}
    输出是
a
b
c
    前两个函数重载看起来很怪异? 原因是, 如果成员函数重载中只要有一个函数是给 rvalue 用的 (也就是函数后面有 && 修饰), 那么其它重载也必须标注上一个 & 号. 读的时候去掉前两个重载的 & 就很容易看明白了. 而最后一个重载正是给 rvalue 用的. 这样变态地使用符号, 看来标准委员会看来是毫不介意 C++ 逐渐演化成既不表意又不象形的新世纪面向对象强化版 Brainfuck. 真的在代码中写这种东西的死 coder 是伤不起的!

Permanent Link: /p/303/

Post tags:

C++

C++11

Function Overload

Left Value

Right Reference

C++ Howto: 在栈上构造一个对象

    C++ 2011 标准引入的各种符号让我感到 C++ 已经快变成一门妖术了. 考虑到很多不明真相的 C 程序员同学们觉得 C++ 妖术是 C with classes, 那么这里先介绍一下 C++ 魔法大典中最基本东西: 怎么运用编译器的魔力在栈上召唤一个对象?

    假设现在有个类叫做 type, 它有个无参数的构造函数, 那么下面的代码
type object();
是巫师学徒们常犯的错误, 这个语句实际上是声明了一个叫做 object 的函数, 它返回一个 type 对象. 施展无参构造魔法的正确咒语是
type object;

    此外, 还有一个很吐血的咒术是
type(object);
同样是定义一个名字为 object 类型为 type 的对象. 是否很震惊? 其实这里的括号是可以任意加的, 如
type object;
type(object);
type((object));
    它们的效果相同. 如果它们写在一起, 后边的会跟第一个发生定义冲突.

    如果现在它有个构造函数需要一个 int 类型参数, 那么下面这两种代码
type object = 0;
type object = type(0);
是不正确的, 哦, 不, 至少是不严谨的, 而且看起来就是土气的 C 程序++, 而不是 C++ 魔法, 正确的咒语是
type object(0);
因为当 type 的构造函数是如下定义时第一种写法是编不过去的
struct type {
    explicit type(int);
};
而第二种写法确实读起来更象是 "调用构造函数, 初始化对象" 两步, 不过它实际上要用到复制构造函数, 虽然很可能编译器有足够强大的魔力优化掉这一次对象复制, 但如果把复制构造函数 delete 掉或者置于 privateprotected 封印之下, 编译器会给出一个恼人的错误.

PS: 在 The C++ Programming Language (C++ 程序设计语言) 一书中, 11.3.3 中有提到一个例子
    ... For example,
     complex b = 3;
    means
     complex b = complex(3);
我认为这一处是错误的, 原因如上所说.

    好, 现在再回到刚才那个括号打水漂的定义方式, 下面这种写法
int a = 3;
type(a);
后面一句是在召唤一个临时的 type 对象么? 当然不是, 编一下就会知道, 那仍然是在定义一个名字为 a 的对象, 即使 type 没有无参构造函数.

Permanent Link: /p/292/

Post tags:

C++

Constructor

Copy Constructor

C++ 异常不怎么常见知识点 --- 后传

    上一篇timothyqiu 同学回复提到通过 std::set_unexpected 指定一个处理方法, 在函数抛出与异常声明不相符的异常时调用. 比如下面这样
#include

#include

void func_throws() throw (std::out_of_range)
{
    throw std::logic_error("danger!");
}

void throw_another()
{
    throw std::out_of_range("safe");
}

int main()
{
    std::set_unexpected(throw_another);
    try {
        func_throws();
    } catch (std::out_of_range e) {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}
    有了这个处理函数后, 确实异常被神奇般地抓住了.

    不过换个写法, 如果处理函数里面也继续抛 std::logic_error
#include
#include

void func_throws() throw (std::out_of_range)
{
    throw std::logic_error("danger!");
}

void throw_another()
{
    throw std::logic_error("not safe!");
}

int main()
{
    std::set_unexpected(throw_another);
    try {
        func_throws();
    } catch (std::out_of_range e) {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}
    程序照崩.

    也就是说, 处理函数相当于替换原来的函数, 在错误处原地重新抛出一个异常, 但异常必须匹配原来函数的异常声明, 否则异常仍然无法捕捉. 明显扯淡么, 随便找两个异常声明不搭界的函数, 这货不就不管用了么, 总不能随抛随设吧.

    当然, 标准还规定了一个很犀利的异常类型, 叫做 std::bad_exception, 如果把这家伙加进异常声明, 那就给力了
#include
#include

void func_throws() throw (std::out_of_range, std::bad_exception)
{
    throw std::logic_error("danger!");
}

void throw_another()
{
    throw;
}

int main()
{
    std::set_unexpected(throw_another);
    try {
        func_throws();
    } catch (std::bad_exception e) {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}
    好了, 原来的异常在 throw_another 里面摇身一变, 成了 std::bad_exception, 不过看 std::cerr 吐出来的东西, 信息都消失了啊, 这不是坑爹么.
    其实说到底, 这跟写成
void throw_another()
{
    throw std::bad_exception("std::bad_exception");
}

Permanent Link: /p/275/

Post tags:

C++

Exception Handling

C++ 异常不怎么常见知识点

    之前也反复有人推荐 Google 的 C++ Coding Style, 昨天看到一位老师写的解读 Google C++ code style 谈对 C++ 的理解, 文后的讨论大多集中在 Google 不允许 C++ 异常. 我这里就不多插嘴了, 不过有些关于异常的基本情况还是想略聊一聊.
    文中的例子使用 g++ 4.5.2 和 clang++ 2.9 编译测试通过.

    如果一个函数声明了异常声明与该函数实际抛出的异常不匹配, 那么这个异常不会被捕获到. 也就是说 catch (...) 不是银弹.
#include


void func_throws() throw (std::out_of_range)
{
    throw std::logic_error("danger!");
}

int main()
{
    try {
        func_throws();
    }
    catch (std::out_of_range) {}
    catch (std::logic_error) {}
    catch (...) {}
    return 0;
}
    补丁: std::set_unexpected 函数与不预期的异常

    而一个有趣的 C++ 规定是, 如果一个函数没有任何异常声明, 则该函数有可能抛出任何异常. 这个规定的一个隐喻是, 一切 C 函数都有可能抛出任何异常. 虽然 C 连异常是什么都不知道, 不过没关系. 因为 C 函数是异常透明的.
    比如下面的例子
#include

extern "C" void exception_pass();

void func_throws()
{
    throw std::logic_error("danger!");
}

void upper_cpp_func()
{
    exception_pass();
}

int main()
{
    try {
        upper_cpp_func();
    } catch (std::logic_error) {
        return 0;
    }
    return 1;
}

extern "C" void exception_pass()
{
    func_throws();
}
    在异常发生后的 stack unwind 阶段, C 函数调用栈帧与 C++ 函数栈帧的处理方式相同. 执行结果是一样的. 但这并不意味着一种好的 C/C++ 编程方式, C 是没有 RAII 的!
    另外, 分离编译再链接时行为会不同, 可能出现异常无法被捕获的情况.

Permanent Link: /p/256/

Post tags:

C

C++

Exception Handling

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
(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 const& pair = std::make_pair(0, 1);
std::stringstream const& strm = std::stringstream();
std::string const& x = "Raki suta";
int const& i = 0;
    这样直白地写出来是有点让人觉得不舒服, 不过作为函数调用的参数则好得多, 比如之前的代码片段 B 那样

Permanent Link: /p/232/

Post tags:

C++

C++11

Copy Constructor

Left Value

Move Semantic

Operator Overload

Right Reference

某 C++ 概念之序篇

    C++ 虔诚的信徒们, 请阅读下面的代码, 回答码后的问题
/* 完整样例 A */
#include

#include

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
#include
#include

struct key_error
    : public std::logic_error
{
    explicit key_error(std::string const& what)
        : std::logic_error(what)
    {}
};

std::string const& lookup(std::map const& map
                        , std::string const& key)
            throw(key_error)
{
    std::map::const_iterator i = map.find(key);
    if (map.end() == i) {
        throw key_error("key ``" + key + "'' not found in map");
    }
    return i->second;
}
问: 上述代码中的 lookup 函数有什么隐患?

答案已揭晓, 点此查看, 谢谢关注.

Permanent Link: /p/222/

Post tags:

C++

C++11

Copy Constructor

Left Value

Move Semantic

Operator Overload

Right Reference

Qt 囧形: QRect

    Qt 中的整点矩形类 QRect 的表示很独特, 存放的是左上角坐标和右下角坐标 (准确地说, 是 int x1; int y1; int x2; int y2;). 对 QRect 取上下左右都是闭区间上的边界, 比如
QRect rect(QPoint(0, 2), QSize(8, 8));
rect.left();   // 0
rect.right();  // 7 : 0 + 8 - 1
rect.top();    // 2
rect.bottom(); // 9 : 2 + 8 - 1
    其实也没什么大问题, 因为确实就是这样. 但是, 自古以来就被训练成左开右闭思维的键盘打击手们, 一不小心就会出错, 比如下面的 Python 列表切片代码示例, 取一个列表的一半, 后一半
list = range(0, 8)
list[0: len(list) / 2]         // [0, 1, 2, 3]
list[len(list) / 2: len(list)] // [4, 5, 6, 7]
    而如果给 QRect "切片", 求一个矩形的左半边和右半边, 代码则看起来像
QRect rect(QPoint(0, 2), QSize(8, 8));

QRect leftHalf(rect);
leftHalf.setRight(leftHalf.left() + leftHalf.width() / 2 - 1);

QRect rightHalf(rect);
rightHalf.setLeft(rightHalf.left() + rightHalf.width() / 2);
    设置右边界的代码修正这个 -1 看起来非常非常不自然. 而在实际运用中, 经常会遇到各种 +1 -1 的修正.

    至于 Nokia 的开发者们自己是不是搞清楚了 QRect 我也不太清楚, 不过 QPainter::drawRect(QRect const&) 这个函数是看起来有 bug 的样子, 比如下面这个例子
#include

#include
#include
#include

struct TestButton
    : public QToolButton
{
    void paintEvent(QPaintEvent*)
    {
        QPainter painter(this);
        painter.setBrush(Qt::NoBrush);

        painter.setPen(QPen(Qt::white, 1));
        painter.drawRect(rect().adjusted(3, 3, -3, -3));

        painter.setPen(QPen(Qt::blue, 1));
        painter.drawRect(rect().adjusted(2, 2, -2, -2));

        painter.setPen(QPen(Qt::green, 1));
        painter.drawRect(rect().adjusted(1, 1, -1, -1));

        painter.setPen(QPen(Qt::red, 1));
        painter.drawRect(rect());
    }
};

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    TestButton button;
    button.show();
    return app.exec();
}
    放大了看, 最外侧红框的右边和下边都不见了.

Permanent Link: /p/219/

Post tags:

C++

QRect

Qt

拿去吧, 四字节整数类型

    对于 C(++) 之流在平台相关这种水深火热之中挣扎的语言, 对机器字长, 以及整数类型字长在一些情况下是极其敏感的. 所以每个大型项目建设之前, 在某个头文件里面定义一大堆 int32 或者是 unsigned8_t 之类的固定字长类型显得非常必要, 而且让工程看起来很专业的样子. 然而, 由于标准库中没有, 项目之间又互相不信任, 结果是几乎每个库都会自定一套, 比如 boost 就有 int8_t, int_least8_t 等类型, 而 Qt 更是拿出了叫做 qulonglong 的类型来卖萌. 虽然说是跨平台, 但是如果迁移平台时使用了错误版本的库呢? 因此有时依然需要写一个类似下面的程序来人肉验证
int main()
{
    std::cout << sizeof(int4_type) << " "
              << sizeof(int8_type) << std::endl;
    return 0;
}
    然而, 人都是不可靠的, 如果有人对你说, 喔, 我跑了上面的程序, 结果是 "4 8", 也许你仍会强迫症一样, 自己再验证一次. 好了, 也许你已经厌烦了, 这么一点破事情, 难道不能交给机器做么?
    理想的解决方案是, 只需要这样一个模板就好了
template

struct IWantAnIntegralTypeOf;

int main()
{
    IWantAnIntegralTypeOf<4> this_is_a_4_bytes_int;
    IWantAnIntegralTypeOf<8> this_is_a_8_bytes_int;
    return 0;
}
不是吗?

    这是完全可行的, 实际上, 可以把 C 支持的所有内建整数类型先拿出来, 组成一个类型链表来搜索, 像下面这样
struct __null__ {};

struct c_char {
    typedef char type;
    typedef __null__ next;
};

struct c_short {
    typedef short type;
    typedef c_char next;
};

struct c_int {
    typedef int type;
    typedef c_short next;
};

struct c_long {
    typedef long type;
    typedef c_int next;
};

struct c_long_long {
    typedef long long type;
    typedef c_long next;
};

struct __head__ {
    typedef c_long_long next;
};
    接下来, 弄个模板来搜, 搜索当然是用模板偏特化的方法
template
struct type_finder;

template
struct type_finder<_TypeNode, _SizeOf, true>
{
    typedef typename _TypeNode::type type;
};

template
struct type_finder<_TypeNode, _SizeOf, false>
{
    typedef
        typename type_finder<
                typename _TypeNode::next
              , _SizeOf
              , _SizeOf == sizeof(typename _TypeNode::next::type)>::type type;
};
    搜索入口则类似
template
struct gimme_the_type {
    typedef typename type_finder<__head__, _SizeOf, false>::type type;
};
    再来调一下最开始想要的那个模板

Permanent Link: /p/217/

Post tags:

C++

Generic Programming

Metaprogramming

Template

std::unique_ptr<cookbook>

    std::unique_ptr 是 C++11 标准的 STL 中, 用来取代 std::auto_ptr 的指针容器 (如果在代码中使用了 std::auto_ptr, 使用 gcc 4.5 之后的版本加上 -std=c++0x -Wall 参数编译时, gcc 会报出警告 std::auto_ptr 已经过时).

    同 std::auto_ptr 一样, std::unique_ptr 的基本功效也能当作简单的自动对象管理指针来使用, 如
#include <memory> /* for std::unique_ptr */
#include


struct echo {
    echo()
    {
        std::cout << "ctor" << std::endl;
    }

    ~echo()
    {
        std::cout << "dtor" << std::endl;
    }

    echo(echo const&) = delete;
};

void test()
{
    std::unique_ptr echo_ptr(new echo);
}

int main()
{
    test();
    std::cout << "." << std::endl;
    return 0;
}
输出应该如
ctor
dtor
.
    类型 echo 在构造和析构时的输出信息可以帮助掌握对象的生命周期. 如上述程序中, echo 对象在 test 函数结束时析构.

    std::unique_ptr 的复制构造函数被 delete 处理了, 所以类似下面的代码会出错
std::unique_ptr a(new int(0));
std::unique_ptr b(a); // ERROR
            // deleted function
            // ‘std::unique_ptr<...>::unique_ptr(std::unique_ptr<...> const&)'
    如果确实需要复制一份整数的值, 需要可以做如下操作
std::unique_ptr b(new int(*a));
    而如果是想要将 a 所持有的指针转移到 b 中, 需要显式调用 std::move 将来移交指针控制权
std::unique_ptr a(new int(0));
std::unique_ptr b(std::move(a));
    此时 a 中的指针会变为 NULL. 明确的转移语义由 std::move 函数表达, 与 std::auto_ptr 暧昧的构造函数不同, std::move 明确指出 std::unique_ptr 对象是通过转移构造进行参数传递的.

    当函数需要使用 std::unique_ptr 作为参数或返回值时, 可以是如下几种形式

Permanent Link: /p/181/

Post tags:

C++

C++11

Move Semantic

Tutorial

unique_ptr

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
**
    _OS& operator<<(_OS& os, my_type const&); // WRONG
private:
    int x;
    double y;
};

template
_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
_OS& operator<<(_OS& os, my_type const& m) // ACTUALLY NOT NEEDED ANY MORE
{
    os << m.str();
    return os;
}
这看起来多疼啊.

    一个可能的方案是, 为每个自定义类型实现 str() 成员函数 (有必要的还可以虚一下), 然后全局声明一个
template
_OS& operator<<(_OS& os, _T const& t)
{
    os << t.str();
    return os;
}
也许这样会好很多.

Permanent Link: /p/177/

Post tags:

C++

Operator Overload

Output Stream

C++ 中捕获整数除零错误

    继承自 C 的优良传统, C++ 也是一门非常靠近底层的语言, 可是实在是太靠近了, 很多问题语言本身没有提供解决方案, 可执行代码贴近机器, 运行时没有虚拟机来反馈错误, 跑着跑着就毫无征兆地崩溃了, 简直比过山车还刺激.
    虽然 C++ 加入了异常机制来处理很多运行时错误, 但是异常机制的功效非常受限, 很多错误还没办法用原生异常手段捕捉, 比如整数除 0 错误. 下面这段代码
#include


int main()
{
    try {
        int x, y;
        std::cin >> x >> y;
        std::cout << x / y << std::endl;
    } catch (...) {
        std::cerr << "attempt to divide integer by 0." << std::endl;
    }
    return 0;
}
输入 "1 0" 则会导致程序挂掉, 而那对 try-catch 还呆在那里好像什么事情都没发生一样. 像 Python 一类有虚拟机环境支持的语言, 都会毫无悬念地捕获除 0 错误.

使用信号

    不过, 底层自然有底层的办法. 这得益于硬件体系中的中断机制. 简而言之, 当发生整数除 0 之类的错误时, 硬件会触发中断, 这时操作系统会根据上下文查出是哪个进程不给力了, 然后给这个进程发出一个信号. 某些时候也可以手动给进程发信号, 比如恼怒的用户发现某个程序卡死的时候果断 kill 掉这个进程, 这也是信号的一种.
    这次就不是 C 标准了, 而是 POSIX 标准. 它规定了哪些信号进程不处理也不会有太大问题, 有些信号进程想处理也是不行的, 还有一些信号是错误中断, 如果程序处理了它们, 那么程序能继续执行, 否则直接杀掉.
    不过, 这些错误处理默认过程都是不存在的, 需要通过调用 signal 函数配置. 方法类似下面这个例子
#include
#include
#include

void handle_div_0(int)
{
    std::cerr << "attempt to divide integer by 0." << std::endl;
    exit(1);
}

int main()
{
    if (SIG_ERR == signal(SIGFPE, handle_div_0)) {
        std::cerr << "fail to setup handler." << std::endl;
        return 1;
    }
    int x, y;
    std::cin >> x >> y;
    std::cout << x / y << std::endl;
    return 0;
}
    可以看出, signal 接受两个参数, 分别是信号编号和信号处理函数. 成功设置了针对 SIGFPE (吐槽: 为什么是浮点异常 FPE 呢?) 的处理函数 handle_div_0, 如果再发生整数除 0 的惨剧, handle_div_0 就会被调用.
    handle_div_0 的参数是信号码, 也就是 SIGFPE, 忽略它也行.

底层机制

Permanent Link: /p/100/

Post tags:

C

C++

Exception Handling

POSIX

Signal

C++ 转移构造

    这篇文章中的例子均可在 GCC 4.5.1 版本和 Clang 2.8 版本编译, 在某些较老或优化不尽相同的编译器上, 下面的例程可能并不能获得与文中所述一致的结果.
    不过, 这篇文章的重点并非讲述 C++ 中关于复制构造函数的优化, 而是为了说明 C++0x 标准中出现的 Move Semantic 所试图解决的问题之一, 所以对关于复制构造函数的例子有大致理解即可.
    重点是, 如果希望完整地编译文中最后一部分中出现的任何 0x 标准相关的代码, 强烈推荐 GCC 4.5.x 版本编译器.

    废话少说, 先来一段代码:
#include


struct cp_ctor_doubling {
    cp_ctor_doubling(int ii)
        : i(ii)
    {}

    cp_ctor_doubling(cp_ctor_doubling const& rhs)
        : i(rhs.i * 2)
    {}

    int const i;
};

cp_ctor_doubling create(int i)
{
    return cp_ctor_doubling(i);
}

int main()
{
    cp_ctor_doubling c(create(10));
    std::cout << c.i << std::endl;
    return 0;
}
问: 输出多少? A. 10 B. 20 C. 40 D. 以上答案都不正确.
    嗯, 也许有得到 B 和 C 的, 得到 D 的同学也许该试着为自己的编译器报个 bug 了, 如果得到 A, 那么恭喜, 您的编译器已经具备对于复制构造函数最基本的优化能力了.
    C 语言忠实信徒揶揄 C++ 编译器会背着程序员做的 "额外的事情" 时, 历来会想到 C++ 的类复制构造函数. 然而, 现实不仅仅是这样, 编译器还会做出更多额外的事情, 比如把不必要的复制构造函数调用优化掉, 或者更准确地说, 把一些临时对象给优化掉, 于是复制构造函数根本不会被调用到.
    如果对上述例子中的编译优化有任何疑虑, 可以尝试下面这段代码
#include

struct cp_ctor_not_impl {
    cp_ctor_not_impl(int ii)
        : i(ii)
    {}

    cp_ctor_not_impl(cp_ctor_not_impl const&);

    int const i;
};

cp_ctor_not_impl create(int i)
{
    return cp_ctor_not_impl(i);
}

int main()
{
    cp_ctor_not_impl c(create(10));
    std::cout << c.i << std::endl;
    return 0;
}

Permanent Link: /p/94/

Post tags:

C++

C++11

Copy Constructor

Move Semantic

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


int main()
{
   std::cout << "Hello, world\n";
}

从这个例子扩展出一个赋值语句几乎毫不费力, 只需要在函数体第一行加个 = 之后随手发挥就好.

#include

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++ 的世界里, 左值跟赋值运算操作符已经没有半点关系了, 倒是只要类型能够匹配上, 运算符重载就能通过编译, 看起来再离奇的算式都没问题.

不过, 如果非要扯赋值运算符, 它跟其它二元运算符有什么不同?

这差异还确实有, 那就是, 赋值运算符必须是成员非静态的, 也就是说下面的编译无法通过

Permanent Link: /p/85/

Post tags:

C++

Left Value

Operator Overload

char** 不能赋值给 char const**

C 和 C++ 允许把指向 "某物" 的指针赋值给指向 "不可改变" 的 "某物" 的指针, 如

char* call = "fsck";
char const* call_const = call;

而且, 如果确定不会更改所知向的对象, 那么推荐加上 const, 这是 C++ 的代码编写建议之一. 但是, 要把指向 “某物” 的二级指针赋值给... 见鬼, 简单的说, 下面这种赋值, 任何一个负责任的 C++ 编译器会报出指针不兼容错误:

char* call = "fsck";
char** ptr_to_call = &call
char const** ptr_const_to_call = ptr_to_call; // error

C/C++ 这样做, 目的其实是在于防范将 char const* 赋值给 char* 的意外行为. 这样说也许很难理解, 那么请看下面这段示例代码

Code Snippet 0-0

char const* CALL = "fsck";

void bind(char const** m)
{
   *m = CALL;
}

int main(void)
{
   char* s;
   bind((char const**)&s); // explicit convert
   s[1] = 'u'; // crash on write
   return 0;
}

在调用 bind 时, 采取强制转换来规避编译错误. 而此时调用 bind 就出现了问题, 它把 char const* 类型的 CALL 变量赋值给了实际上是 char*s, 结果就是在紧接下来的那个赋值中, 程序由于尝试去写只读内存区而崩溃. 有崩溃有真相, 这就是对标题的解答.

另一个类似的例子是, 子类对象的二级指针不能赋值给父类对象的二级指针, 如

Code Snippet 0-1

struct base {};
struct inherit : base {};

void f(void)
{
   inherit** i = NULL;
   base** b = i; // error
}

Permanent Link: /p/83/

Post tags:

C

C++

const

Page 1 2


. Back to Tobary book
Tobary book - Copyright (C) ZhePlus @ Bit Focus
About