About
RSS

Bit Focus


正确重载 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 Load full text

Post tags:

 Operator Overload
 C++
 RAII

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 Load full text

Post tags:

 RAII
 Exception Handling
 C++

从 Python 的 with 到 RAII

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

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 Load full text

Post tags:

 Exception Handling
 C++
 RAII
 Python

Python: try finally with 简介

    用 Python 做一件很平常的事情: 打开文件, 逐行读入, 最后关掉文件; 进一步的需求是, 这也许是程序中一个可选的功能, 如果有任何问题, 比如文件无法打开, 或是读取出错, 那么在函数内需要捕获所有异常, 输出一行警告并退出. 代码可能一开始看起来是这样的
def read_file():
    try:
        f = open('yui', 'r')
        print ''.join(f.readlines())
    except:
        print 'error occurs while reading file'
    finally:
        f.close()
    不过这显然无法运作, 因为 f 是在 try 块中定义的, 而在 finally 中无法引用.

    如果将 f 提取到 try 块外部, 如
def read_file():
    f = open('azusa', 'r')
    try:
        print ''.join(f.readlines())
    except:
        print 'error occurs while reading file'
    finally:
        f.close()
那么, 问题在于当打开文件失败, 抛出异常将不会被捕获.

    挫一点的方法自然是, 再套一层 try
def read_file():
    try:
        f = open('sawako', 'r')
        try:
            print ''.join(f.readlines())
        except:
            print 'error occurs while reading file'
        finally:
            f.close()
    except:
        print 'error occurs while reading file'
    当然这不仅仅是多一层缩进挫了, 连警告输出都白白多一次呢.

    正规一点的方式是, 使用 Python 引入的 with 结构来解决, 如
def readFile():
    try:
        with open('mio', 'r') as f:
            print ''.join(f.readlines())
    except:
        print 'error occurs while reading file'
    当文件打开失败时, 异常自然会被 except 到; 否则, 在 with 块结束之后, 打开的文件将自动关闭.

    除了打开文件, 还有其它这样可以用于 with 的东西么? 或者说, 怎么自定义一个什么东西, 让它能用于 with 呢?
    直接回答后一个问题吧, 秘密在于 Python 虚拟机在 with 块退出时会去寻找对象的 __exit__ 方法并调用它, 把释放资源的动作放在这个 __exit__ 函数中就可以了; 另外, 对象还需要一个 __enter__ 函数, 当进入 with 块时, 这个函数被调用, 而它的返回值将作为 as 后引用的值. 一个简单的例子是

Permanent Link: ?p=328 Load full text

Post tags:

 Exception Handling
 Python
 RAII


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site