aiyun-li 2014-07-12T21:21:22-07:00 ccf.developer@gmail.com Implicit conversions 2014-05-28T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/05/28/implicit-conversions 该博文翻译自Implicit conversionts

目录

转换优先级
左值转换
    左值到右值转换
    数组名到指针的转换
    函数指针的转换
数值提升
    整型提升
数值转换
    整型转换
    浮点转换
    浮点-整型转换
    指针转换
    指向成员指针转换
布尔转换
修饰符转换
安全bool值问题

隐式转换发生在将任何表达式需要类型T1应用在某上下文不接受该类型,但是接受其他某类型T2的场景中,特别的是:

  • 类型T1表达式应用于接受类型为T2的函数参数的函数调用中
  • 类型T1是某个希望得到类型为T2的操作符上
  • 初始化类型为T2的变量,包括函数返回值
  • 类型为T1的表达式应用于switch语句中(T2是整形变量)
  • 表达式应用于if语句中(T2是bool型变量)

在以上场景中,当仅存在一个从T1到T2的隐式转换时,程序可以正常编译。如果存在多个函数或者运算符重载函数被调用,当从T1到T2的每一个可能的隐式转换发生后,重载决议才会执行。

转换的优先级


隐式转换的序列的优先级如下:

  • 0或者1个标准转换序列
  • 0或者1个用户定义的转换
  • 0或者1个标准转换序列

当考虑构造函数或者用户自定义的转换函数时,只有标准的转换序列是允许的(否则,用户自定义的转换会形成转换链)。当从语言的内置类型之间转换时,只有标准转换序列是允许的。

一个标准的转换序列由如下组成,其优先级如下:

  • 0或者1个左值转换
  • 0或者1个数值提升或者数值转换
  • 0或者1个资格调整(Qualification Adjustment)

用户定义的转换由如下组成:

  • 0或者1个non-explicit single-argument构造函数或者non-explicit转换函数调用

一个表达式e可以隐式转换为T2,当且仅当T2是从e复制初始化(copy-initialization),即T2 t = e。注意这和直接构造初始化不一样 T2 t(e),此时需要额外考虑显式的构造函数和转换函数。

上述规则的一个例外是如下五个上下文中特殊的隐式转换,此时需要bool类型的变量:

  • if, while, for中的控制语句
  • 逻辑运算符,!,&&, ||
  • 条件运算符: ?:
  • static_assert
  • noexcept

在上述上下文环境中隐式转换按如下方式进行 bool t(e)。用户显式定义的转换函数:T::operator bool()const将会被考虑。这些表达式contextually convertible可转换为bool。

在如下上下文中,需要上下文相关的类型T,只有当类类型E仅有一个用户定义的转换函数时,且其返回值是cv T或者是cv T的引用。

  • 新表达式中的数组界限(T is std::size_t)
  • 数组界限的声明(T is std::size_t)
  • 删除运算符中的参数(T is any object pointer type)(since c++14)
  • 整形常量,使用字面类型(T is any integral or unscoped enumeration type and the selected user-defined conversion function is constexpr)

表达式e是contextually implicit转换成T。

左值转换


左值转换是指在需要右值的上下文中提供左值。

左值到右值的转换


任何非函数,非数组的类型T的glvalue可以被隐式转换成相同类型的prvalue。如果T是非类类型,这种转换会移除cv修饰。除非遇到不估值的上下文,例如sizeof,typeid, noexcept, decltype,这种转换会使用原来的glvalue为构造函数的参数,复制构造类型为T的临时变量,临时变量会以prvalue形式返回。如果glvaule是nullptr_t,返回的变量值为nullptr。

数组名到指针的转换


类型是长度为N,类型T数组的左值或者右值,或者是未知长度类型T的数组可以隐式转换为指向T的prvalue。产生的指针指向数组的第一个元素。

函数到指针的转换


函数类型T的左值可以隐式转换为一个指向该函数的prvalue。这个不适用于non-static成员函数,因为指向非静态成员函数的左值不存在。

数值提升


整型提升


小整型的prvalue值(例如char)会转换成表示范围个更大的整型(例如int)。特别是在算数操作符不接受类型比int小的数作为参数,整型提升自动执行。这种转换保持之前的值。

下面是一些整型提升的场景:

  • signed char或者signed short转换成int
  • unsigned char或者unsigned short转换成int。如果int可以表示unsigned int,unsigned int也转换为int。
  • char可以转变成int或者unsigned int,取决于底层的是signed char还是unsigned char
  • wchar_t, char16_t和char32_t可转变成下列类型(保证可以容纳下所有的值):int, unsigned int, long, unsigned long, long long, unsigned long long。
  • unscoped enumeration类型,如果它的潜在类型没有规定,那么它会按以下列表转换(保证可以容纳其所有的值):int, unsigned int, long, unsigned long, long long, unsigned long long。如果值的范围太大,那么没有整型提升。
  • 位域类型:如果int可以表示其所有范围的值,则转换成int;否则转换成unsigned int;其它情况没有整型提升。

枚举类型如果底层实现类型指定了,其整型提升按照指定的类型提升规则进行提升。

浮点提升


float类型转换成double,其值不变。

数值转换


与提升不同,数值转换可能会改变值,造成精度的丢失。

整型转换


整型和unscoped枚举类型可以转换成任何其它的整型值。如果转换是按如下方式进行,那么是整型提升,不是整型转换。

  • 如果目标类型是unsigned,结果的类型是使用模(2^n)的得到的最小无符号数值。其中n是目标类型的长度。依据目标类型是宽或者窄,有符号整数是用符号位扩展或者截断;无符号数0扩展或者截断。
  • 如果目标类型是signed,如果目标的值可以用相应的类型表示则值不变;否则结果是实现相关的。
  • 如果源类型是bool,false是0,true是1(如果目标类型是int,则是整型提升,不是整数转换)
  • 如果目标类型是bool,这是bool转换。

浮点转换


浮点类型的值可以转换任何其它的浮点类型。如果转换是按下述进行,那么是浮点提升,不是转换:

  • 源类型可以由目标类型精确表示,其值不变
  • 源类型表示成目标类型两个值之间的某个值,结果是两个值之一,是实现相关的。
  • 其它情况,未定义。

浮点-整型转换


  • 浮点类型可以转换成任意的整型,小数部分直接被截断(直接丢弃)。如果截断后的值没法用目标类型表示,那么行为是未定义的。如果目标类型是bool,则是bool转换。
  • 整型或者unscoped枚举类型是可以转换任意浮点类型。如果其值无法精确表示,则由实现定义选择最接近最大值或者最小值来表示。如果其值无法用指定类型表示,行为未定义。如果源类型是bool,false是0,ture是1.

指针转换


  • 空指针是NULL常量,为0的整型值或者std::nullptr_t类型,包括nullptr,可以转换成任意类型,结果是转换后类型的空指针。这种转换(又被称为空指针转换)可以一次变成cv修饰的类型,意思是说这不是数值类型转换和修饰符转换的组合。
  • 指向任意目标类型T(cv修饰是可选的)的指针可以转换成void(相同的cv修饰符)。所得类型在内存布局上一致的。如果原指针是空指针,则结果是相应类型的空指针。
  • 指向派生类类型的指针(cv修饰是可选的)可以转换成相应的基类类型(相同的cv修饰)。转换的结果是指向原来对象中subobject的基类部分的指针。空指针是转换成相应类型的空指针。

指向成员指针的转换


  • 空指针是NULL常量值,值为0的整型值或者std::nullptr_t类型,包括nullptr,可以转换成指向成员的指针,结果是指向相应类型的空指针。
  • 指向某类型T基类B成员的指针可以转换成相同类型T的派生类中的成员指针。如果B无法访问或者未定义或者D的虚基类或者是D基类的基类,转换是ill-formed(不会编译)。结果类型可以解引用为D对象,其可以访问D类中为B的subojbect。空指针还是转换成相应类型的空指针。

布尔转换


整型、浮点、枚举、指针以及指向成员的指针类型可以转换成bool。0值(整型、浮点和枚举)以及空指针,指向成员的空指针转换成false,其它值是true。

修饰符转换


  • 指向cv修饰符修饰的指针,可以转换为更多cv修饰符修饰的指针。
  • 指向cv修饰符修饰的成员类型的指针,可以转换更多cv修饰符修饰的指针。
  • 没有修饰符:增加const
  • 没有修饰符:增加volatile
  • 没有修饰符:增加const volatile
  • const修饰:变成 const volatile
  • volatile修饰:变成 const volatile

安全bool值问题


直到C++11中引入的显示转换,设计一个可以用在需要布尔值的上下文是一个问题:考虑用户自定义的转换函数,例如T::operator bool() const,隐式的转换顺序允许在函数调用后有额外的转换,即bool值可以转换为int,这样像obj<<1,或者 int i = obj是合法的。

早期的解决方法可以在std::basic_ios中发现,它定义了operator!和operator void*(直到c++11),所以当这样的代码:if(std::cin)编译成为void*,然后转换成bool值,但是int n = std::cout不编译,因为void*无法转换成int。但是这仍然允许一些奇怪的代码,例如 delete std::cout,在C++11之前的一些第三方库设计一些更加优雅的解决方法,称为Safe Bool idiom

]]>
Multitasking Management in the Operating System Kernel 2014-05-15T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/05/15/multitask-management-in-os 这篇文章翻译自KUKURUKU

系统中的所有任务共享一个处理器上的时间,我们需要做多任务管理。或者说伪多任务,因为所有任务是共享一个处理器上的时间。首先,我会介绍多任务的类型(协作式/抢占式);然后我会继续介绍抢占式多任务中的调度原则。这篇文章已经针对第一次想从内核级别上理解多任务的读者做了优化。但是文中的实例都可以编译运行,那些已经熟悉理论,但是从未真正“尝试”调度器的读者也会感兴趣。

介绍

首先介绍一下多任务的含义,下面是维基百科的定义:

In computing, multitasking is a method where multiple tasks, also known as processes, are performed during the same period of time. The tasks share common processing resources, such as a CPU and main memory. In the case of a computer with a single CPU, only one task is said to be running at any point in time, meaning that the CPU is actively executing instructions for that task. Multitasking solves the problem by scheduling which task my by the one running at any given time, and when another waiting task gets a turn. The act of reassigning a CPU from one task to another one is called a context switch.

在上述定义中提到了资源共享和调度的概念,本文在后面部分会继续关注,我会基于线程来描述线程。

所以,我们需要再引入一个概念,让我们称之为调度线程(scheduling thread)。它是CPU在程序运行期间按顺序执行的一系列指定集合。

我们提到了多任务,当然,在系统是可以有多个计算线程。当前处理器正在执行指令所属的线程的状态为活跃(active)。因为在单处理器系统中,在某一个时刻只有一个指令可以执行,所以只有一个线程处于活跃状态。处理器选择active线程的过程称为调度,负责线程选择的模块称为调度器。

Thread1

在系统中有多种调度方法,基本上可以分为两个大类:

  • 协作式(Cooperative),调度器不能剥夺正在计算的线程的时间,除非它主动放弃
  • 抢占式(Preemtive),当时间片结束,调度器选择下一个active的线程;该线程也可以放弃它所剩余的时间片

首先来分析一下协作调度,因为它较容易实现。

协作调度器

本文所考虑的协作调度器很简单,所列举的例子适合初学者,以为这样他们更容易理解多任务。理解理论概念的读者可以直接跳至“抢占式调度器”部分继续阅读。

基本协作调度器

假设我们有多个任务,运行时间很短,我们可以依次调度它们。我们将利用一些带参数的普通函数来描述任务。调度器会在这些函数组成的数组上完成操作。它会调用任务函数的初始化函数。当每一个任务所有必需的操作都执行后,函数会返回主调度器循环执行。

#include <stdio.h>

#define TASK_COUNT 2
stuct taks
{
    void (*func)(void *);
    void *data)
};

static struct task tasks[TASK_COUNT];

static void scheduler(void)
{
    int i;
    for (int i = 0; i < TASK_COUNT; ++i)
    {
        tasks[i].func(tasks[i].data);
    }
}

static void worker(void *data)
{
    printf("%s\", (char*)data);
}

static struct task *task_create(void (*func)(void*), void *data)
{
    static int i = 0;

    tasks[i].func = func;
    tasks[i].data = data;

    return &tasks[i++];
}

int main(void)
{
    task_create(&worker, "First");
    task_create(&worker, "Second");

    scheduler();

    return 0;
}

Output Result:

First

Second

CPU使用图如下:

Graph CPU Usage

带基本事件处理的协作调度器

当然,上面例子中的调度器太多原始。让我们引入一种激活任务的功能。为了实现此目的,我们需要在结构中增加一个标记。该标记指示任务是否处于激活状态,我们需要增加一些API来管理该激活过程。

#include <stdio.h>

#define TASK_COUNT 2

struct task
{
    void (*func)(void*);
    void *data;
    int activated;
};

static struct task tasks[TASK_COUNT];

struct task_data
{
    char *str;
    struct task *next_task;
};

static struct task *task_create(void (*func)(void*), void *data)
{
    static int i = 0;

    tasks[i].func = func;
    tasks[i].data = data;

    return &tasks[i++];
}

static int task_activate(struct task *task, void *data)
{
    task->data = data;
    task->activated = 1;

    return 0;
}

static int task_run(struct task *task, void *data)
{
    task->activated = 0;
    task->func(data);

    return 0;
}

static void scheduler(void)
{
    int i;
    int fl = 1;

    while (fl)
    {
        fl = 0;
        for (i = 0; i < TASK_COUNT; ++i)
            if (tasks[i].activated)
            {
                fl = 1;
                task_run(&tasks[i], tasks[i].data);
            }
    }
}

static void worker1(void *data)
{
    printf("%s\n", (char*) data);
}

static void worker2(void *data)
{
    struct task_data *task_data;
    task_data = data;

    printf("%s\n", task_data->str);

    task_activate(task_data->next_task, "First activated");
}

int main(void)
{
    struct task *t1, *t2;
    struct task_data task_data;

    t1 = task_create(&worker1, "First create");
    t2 = task_create(&worker2, "Second create");

    task_data.next_task = t1;
    task_data.str = "Second activated";

    task_activate(t2, &task_data);

    scheduler();

    return 0;
}

Output Result:

Second activated

First activated

CPU使用图如下:

CPU

带消息队列的协作调度器

上面方案的问题很明显。如果某个人想将同一个任务激活两次,除非该任务执行结束,否则它不可能做到,第二次激活的信息被阻塞了。该问题可以通过消息队列的方式部分解决。我们加入一个数组取代标记,它将存储每个线程的消息队列。

#include <stdio.h>
#include <stdlib.h>

#define TASK_COUNT 2

struct message {
    void *data;
    struct message *next;
};

struct task {
    void (*func)(void *);
    struct message *first;
};

struct task_data {
    char *str;
    struct task *next_task;
};

static struct task tasks[TASK_COUNT];


static struct task *task_create(void (*func)(void *), void *data) {
   static int i = 0;

   tasks[i].func = func;
   tasks[i].first = NULL;

   return &tasks[i++];
}

static int task_activate(struct task *task, void *data) {
    struct message *msg;

    msg = malloc(sizeof(struct message));
    msg->data = data;
    msg->next = task->first;

    task->first = msg;

    return 0;
}

static int task_run(struct task *task, void *data) {
   struct message *msg = data;

   task->first = msg->next;

    task->func(msg->data);

    free(data);

   return 0;
}

static void scheduler(void) {
   int i;
   int fl = 1;
    struct message *msg;

    while (fl) {
        fl = 0;

        for (i = 0; i < TASK_COUNT; i++) {
        while (tasks[i].first) {
                fl = 1;
                msg = tasks[i].first;
                task_run(&tasks[i], msg);
        }
        }
    }
}


static void worker1(void *data) {
   printf("%s\n", (char *) data);
}

static void worker2(void *data) {
   struct task_data *task_data;

   task_data = data;

   printf("%s\n", task_data->str);

   task_activate(task_data->next_task, "Message 1 to first");
   task_activate(task_data->next_task, "Message 2 to first");
}

int main(void) {
   struct task *t1, *t2;
   struct task_data task_data;

   t1 = task_create(&worker1, "First create");
   t2 = task_create(&worker2, "Second create");

   task_data.next_task = t1;
   task_data.str = "Second activated";

   task_activate(t2, &task_data);

   scheduler();

   return 0;
}

Output Results:

Second activated

Message 2 to first

Message 1 to first

CPU使用情况如下:

CPU

带调用顺序存储的协作调度器

上述方案的另外一个问题是任务激活的顺序没有存储。每一个任务安排自己的优先级,通常这并不好。为了解决这个问题,可以通过再创建一个消息队列和一个检测程序来解决这个问题。

#include <stdio.h>
#include <stdlib.h>

#define TASK_COUNT 2

struct task {
    void (*func)(void *);
    void *data;
    struct task *next;
};

static struct task *first = NULL, *last = NULL;

static struct task *task_create(void (*func)(void *), void *data) {
    struct task *task;

    task = malloc(sizeof(struct task));
    task->func = func;
    task->data = data;
    task->next = NULL;

    if (last) {
        last->next = task;
    } else {
        first = task;
    }

    last = task;

    return task;
}

static int task_run(struct task *task, void *data) {

    task->func(data);

    free(task);

   return 0;
}

static struct task *task_get_next(void) {
    struct task *task = first;

    if (!first) {
        return task;
    }

    first = first->next;
    if (first == NULL) {
        last = NULL;
    }

    return task;
}

static void scheduler(void) {
    struct task *task;

    while ((task = task_get_next())) {
        task_run(task, task->data);
    }
}

static void worker2(void *data) {
    printf("%s\n", (char *) data);
}

static void worker1(void *data) {
    printf("%s\n", (char *) data);

    task_create(worker2, "Second create");
    task_create(worker2, "Second create again");
}


int main(void) {
   struct task *t1;

   t1 = task_create(&worker1, "First create");

   scheduler();

   return 0;
}

Outpt Result:

First create

Second create

Second create again

CPU使用情况如下:

CPU

在继续讲解抢占式调度器之前,我想说明在实际系统中任务切换的代价很低。这个途径需要程序员特别的关注,它需要注意到在性能测试时不要让任务死循环。

抢占式调度器

Preemptive

首先做如下假设。在同一个执行的程序中有个计算线程。在任意有个指令执行期间,调度器可以中断一个激活的线程,激活另外一个线程。为了管理这种任务,向协作调度器那样保存任务的执行函数和参数是不够的。你必须至少知道当前执行指令的地址和每个任务局部变量的集合的地址。所以,你必须保存每个任务变量的备份。因为线程中的局部变量是被隔开的,必须分配一些空间来保存线程的栈;那个地方也应该存储栈的当前位置。

这些数据:指令指针和栈指针会存储在处理器寄存器中。除了他们,为了正常工作还要保存其它信息,这些信息也存储在寄存器中,包括状态标记,不同通用目的寄存器(存储临时变量)等。上述所有的信息称为CPU上下文(CPU context)。

CPU context

CPU context

处理器上下文(Processor context/CPU context)是一种存储处理器寄存器内部状态的数据结构。该上下文必须把处理器转至一个计算线程执行的正确状态。处理器处理的线程由一个切换至另外一个称为上下文切换(context switch)。

我们项目中x86架构中的上下文数据结构描述如下:

struct context {
    /* 0x00 */uint32_t eip; /**< instruction pointer */
    /* 0x04 */uint32_t ebx; /**< base register */
    /* 0x08 */uint32_t edi; /**< Destination index register */
    /* 0x0c */uint32_t esi; /**< Source index register */
    /* 0x10 */uint32_t ebp; /**< Stack pointer register */
    /* 0x14 */uint32_t esp; /**< Stack Base pointer register */
    /* 0x18 */uint32_t eflags; /**< EFLAGS register hold the state of the processor */
};

CPU上下文和上下文切换的概念是理解抢占式调度器的基础。

Context switch

上下文切换是指线程执行的切换。调度器保存当前的上下文,然后将处理器的寄存器恢复至另外一个上下文。

在之前我已经提到:调度器可以中断任何一个激活的线程,这实际上在某些方面简化模式。现实中,不仅调度器会中断线程,CPU在响应外部硬件中断事件时也会中断线程。,执行中断处理后会将控制权返回调度器。例如,假设外部事件是系统计时器,它负责记录激活线程的所使用的时间片。我们假设系统只有系统计时器一个中断源,那么处理的时间图和下图相近:

Time slice

在x86架构上的上下文切换过程如下:

.global context_switch
context_switch:
    movl 0x04(%esp), %ecx       /* Point ecx to previous registers */
    movl (%esp), %eax           /* Get return address */
    movl %eax, CTX_X86_EIP(%ecx)   /* Save it as eip */
    movl %ebx, CTX_X86_EBX(%ecx)   /* Save ebx */
    movl %edi, CTX_X86_EDI(%ecx)   /* Save edi */
    movl %esi, CTX_X86_ESI(%ecx)   /* Save esi */
    movl %ebp, CTX_X86_EBP(%ecx)   /* Save ebp */
    add $4, %esp                /* Move esp in state corresponding to eip */
    movl %esp, CTX_X86_ESP(%ecx)   /* Save esp */
    pushf                       /* Push flags */
    pop  CTX_X86_EFLAGS(%ecx)   /* ...and save them */

    movl 0x04(%esp), %ecx       /* Point ecx to next registers */
    movl CTX_X86_EBX(%ecx), %ebx   /* Restore ebx */
    movl CTX_X86_EDI(%ecx), %edi   /* Restore edi */
    movl CTX_X86_ESP(%ecx), %esi   /* Restore esp */
    movl CTX_X86_EBP(%ecx), %ebp   /* Restore ebp */
    movl CTX_X86_ESP(%ecx), %esp   /* Restore esp */
    push CTX_X86_EFLAGS(%ecx)   /* Push saved flags */
    popf                        /* Restore flags */
    movl CTX_X86_EIP(%ecx), %eax   /* Get eip */
    push %eax                   /* Restore it as return address */

    ret

线程状态模型

抢占式调度器和协作调度器的一个明显区别是上下文出现的频率。我们看看线程从创建到结束发生了什么。

Thread State Machine

  • 初始状态(init),负责线程的创建,但是并没有假如执行队列。当初始状态完成后,线程完成创建,但是并没有释放内存
  • 执行(run)状态很明显,线程由CPU运行
  • 就绪(ready)状态表示线程并没有执行,但是等待CPU赋予其时间片,线程处于调度器的队列中

但是一个线程还有其它的状态。当其等待其它时间时,其可以放弃时间片。例如,它可以进入休眠状态,然后继续从休眠的时刻开始执行。

因此一个线程在某个时刻可以出于任何状态(就绪到运行,休眠等),而在协作调度器中,只用一个标记来标记线程的活动就足够了。

通用的线程状态转移图如下:

General State Machine

在上述图中,出现了一个等待状态。它通知调度器其出于休眠状态,知直到其唤醒前,其不需要处理器时间片。接下来,让我们考虑一下抢占式调度器中的API,深入理解线程的状态。

State Implementation

如果仔细观察状态图,你会发现初始状态和等待状态几乎没有区别。他们的下一个状态都只能是就绪状态,它们告诉调度器它们已经准备好获取时间片了,所以初始状态是多余的。

现在看一下退出状态,它具有自己的特殊性。下面将描述它的退出函数在调度器中具体指明。

线程可以有两种方式退出。第一个是线程完成自己的函数执行,释放自己申请的空间;第二个是线程负责资源释放。在第二种情况下,线程可以观测到其它线程释放它自己的资源,通知它执行结束,将控制权返回调度器。在第一种情况下,线程自己释放资源,然后将控制权返回调度器。当调度器获取控制权以后,线程不会继续执行。在两种情况下的退出状态含义相同-线程在这种状态下不想再获取时间片,它不需要再次进入调度器的队列。它不同于等待状态,所以你不需要再创建一个单独的状态。

所以,我们有三种状态,我们将这些状态分成三个独立的域,我们可以使用一个整型变量存储,但是这个使用这种方式是为了简化。所以线程由如下三个状态:

  • 激活状态(active),开始并由处理器执行
  • 等待状态(waiting),等待某事件发生,它和init和exit事件相同
  • 就绪状态(ready),由调度器控制。线程目前在调度器队列中或者开始在处理器上执行。这个状态相对于图中的就绪状态含义更广。在大多数情况下,激活和就绪,就绪和等待在理论上是正交的,但是有一些中间态破坏了这种正交性。下面会仔细阐述该问题。

Creation

线程执行包括所有必需的初始化(thread_init函数),以及一个可能的线程开始。在初始化时,栈上的空间会被释放,处理器上下文会确定,必要的标记以及其它的基本信息。

当创建一个线程时,我们遍历就绪的线程队列,该队列由调度器在任何时间使用。所以我们必须在所有结构完成初始化之前阻止调度器在线程上的操作。当线程的初始化完成后,线程进入等待状态,它与初始状态相同。在这之后,根据传入的参数,我们要么开始线程的运行,要么不开始。线程的开始函数时值调度器里的唤醒函数,后面会详细描述。我们将认为该函数只是将线程放入调度器的队列,然后将线程从等待状态变为就绪状态。线程创建thread_create和初始化thread_init函数如下:

struct thread *thread_create(unsigned int flags, void *(*run)(void *), void *arg) {
    int ret;
    struct thread *t;

//…

    /* below we are going work with thread instances and therefore we need to
        * lock the scheduler (disable scheduling) to prevent the structure being
        * corrupted
        */
    sched_lock();
    {
        /* allocate memory */
        if (!(t = thread_alloc())) {
                t = err_ptr(ENOMEM);
                goto out;
        }

        /* initialize internal thread structure */
        thread_init(t, flags, run, arg);

    //…

    }
out:
    sched_unlock();

    return t;
}



void thread_init(struct thread *t, unsigned int flags,
        void *(*run)(void *), void *arg) {
    sched_priority_t priority;

    assert(t);
    assert(run);
    assert(thread_stack_get(t));
    assert(thread_stack_get_size(t));

    t->id = id_counter++; /* setup thread ID */

    dlist_init(&t->thread_link); /* default unlink value */

    t->critical_count = __CRITICAL_COUNT(CRITICAL_SCHED_LOCK);
    t->siglock = 0;
    t->lock = SPIN_UNLOCKED;
    t->ready = false;
    t->active = false;
    t->waiting = true;
    t->state = TS_INIT;

    /* set executive function and arguments pointer */
    t->run = run;
    t->run_arg = arg;

    t->joining = NULL;

//...

    /* cpu context init */
    context_init(&t->context, true); /* setup default value of CPU registers */
    context_set_entry(&t->context, thread_trampoline);/*set entry (IP register*/
    /* setup stack pointer to the top of allocated memory
     * The structure of kernel thread stack follow:
     * +++++++++++++++ top
     *                  |
     *                  v
     * the thread structure
     * xxxxxxx
     * the end
     * +++++++++++++++ bottom (t->stack - allocated memory for the stack)
     */
    context_set_stack(&t->context,
            thread_stack_get(t) + thread_stack_get_size(t));

    sigstate_init(&t->sigstate);

    /* Initializes scheduler strategy data of the thread */
    runq_item_init(&t->sched_attr.runq_link);
    sched_affinity_init(t);
    sched_timing_init(t);
}

Waiting Mode

线程由于某些原因,其可以切换至其它状态,例如,调用了sleep函数,当前线程从运行状态切换至等待状态。在协作调度器中,我们仅用一个标记来标记这种状态,在抢占式调度器中,为了不丢失线程,我们将线程放在另外一个特殊的队列中。例如,当尝试获取已被占用的互斥锁时,在进入休眠前,线程将自己放入等待互斥锁的队列中。之后当线程等待的事件发生时,线程将被唤醒,我们将线程返回至完成的队列中。

Thread Termination

线程处于等待状态。如果线程执行了相应的函数,并且正常退出,它所占用的资源应该被收回。我已经在退出状态是冗余的部分描述过这个过程。所以,我们看看具体怎么实现:

void __attribute__((noreturn)) thread_exit(void *ret) {
    struct thread *current = thread_self();
    struct task *task = task_self();
    struct thread *joining;

    /* We can not free the main thread */
    if (task->main_thread == current) {
        /* We are last thread. */
        task_exit(ret);
        /* NOTREACHED */
    }

    sched_lock();

    current->waiting = true;
    current->state |= TS_EXITED;

    /* Wake up a joining thread (if any).
        * Note that joining and run_ret are both in a union. */
    joining = current->joining;
    if (joining) {
        current->run_ret = ret;
        sched_wakeup(joining);
    }

    if (current->state & TS_DETACHED)
        /* No one references this thread anymore. Time to delete it. */
        thread_delete(current);

    schedule();

    /* NOTREACHED */
    sched_unlock();  /* just to be honest */
    panic("Returning from thread_exit()");
}

Jumping-off Place for Processing Function Call

我们不止一次提到,当线程结束运行时,它应该释放其占有的资源。在线程执行其函数,按惯例,我们很少需要以非正常的方式结束它,所以我宁愿不调用thread_exit函数。此外,我们需要准备初始化上下文,但是也没必要每次都执行。所以线程将执行一个thread_trampoline cover-function。它用来初始化上下文和正确结束线程。

static void __attribute__((noreturn)) thread_trampoline(void) {
    struct thread *current = thread_self();
    void *res;

    assert(!critical_allows(CRITICAL_SCHED_LOCK), "0x%x", (uint32_t)__critical_count);

    sched_ack_switched();

    assert(!critical_inside(CRITICAL_SCHED_LOCK));

    /* execute user function handler */
    res = current->run(current->run_arg);
    thread_exit(res);
    /* NOTREACHED */
}

总结:线程结构描述

Thread structure description

为了更加清楚滴描述抢占式调度器,我们需要一个复杂的数据结构,它包括:

  • 处理器寄存器的信息(上下文)。
  • 任务状态的信息,处于就绪等待执行状态或者等待资源的回收。
  • 标记符。就像数组有索引一样。但是如果线程需要增加和删除,最好使用队列,这个时候标记就会很重要。
  • 开始函数和参数,也许需要返回值。
  • 一块内存空间的地址,为任务执行分配,在线程退出时需要回收。

所以数据结构描述如下:

struct thread {
    unsigned int        critical_count;
    unsigned int        siglock;

    spinlock_t          lock;           /**< Protects wait state and others. */

    unsigned int        active;         /**< Running on a CPU. TODO SMP-only. */
    unsigned int        ready;          /**< Managed by the scheduler. */
    unsigned int        waiting;        /**< Waiting for an event. */

    unsigned int        state;          /**< Thread-specific state. */

    struct context      context;        /**< Architecture-dependent CPU state. */

    void                *(*run)(void *); /**< Start routine. */
    void                *run_arg;       /**< Argument to pass to start routine. */
    union {
        void            *run_ret;       /**< Return value of the routine. */
        void            *joining;       /**< A joining thread (if any). */
    } /* unnamed */;

    thread_stack_t      stack;          /**< Handler for work with thread stack */

    __thread_id_t       id;             /**< Unique identifier. */

    struct task         *task;          /**< Task belong to. */
    struct dlist_head  thread_link;  /**< list's link holding task threads. */

    struct sigstate     sigstate;       /**< Pending signal(s). */

    struct sched_attr  sched_attr;   /**< Scheduler-private data. */
    thread_local_t      local;
    thread_cancel_t     cleanups;
};

上面结构中有一些在本文中并没有解释(sigstate, local, cleanups)。他们是为了支持POSIX线程,与本文描述的主题不是很相关。

调度器和调度策略

再次提醒一下读者我们现在有线程的结构,包括上下文,我们在上下文之间切换。此外,我们有一个系统计数器记录时间片。换句话说,我们有调度器的运行环境。调度器的任务是在线程之间分配时间片。为了决定下一个激活的线程,调度器有一个它可以操作的队列,其存储了就绪线程。调度器决定下一个时刻该调度线程的准则称为调度策略。调度策略的主要功能是操作包括就绪线程的队列:增加,删除,决定下一个激活线程。调度器的表现依赖于这些函数的执行。因为我们已经定义了独立的概念,我们将将其划分成一个实体,其接口描述如下:

extern void runq_init(runq_t *queue);
extern void runq_insert(runq_t *queue, struct thread *thread);
extern void runq_remove(runq_t *queue, struct thread *thread);
extern struct thread *runq_extract(runq_t *queue);
extern void runq_item_init(runq_item_t *runq_link);

让我们考虑调度策略的实现细节。

调度策略的例子

我将只考虑最简单的调度策略作为例子,这样我们就把注意力集中在抢占式调度器的独有特点上。在调度策略中,在不考虑优先级时,线程将被依次处理。一个新的线程或者一个刚刚用完时间片的线程将放置在队列末端。线程从顶端开始依次获取处理器资源。队列用双向列表实现。当我们增加一个元素时,我们将其放置在末端,当我们删除时,从顶端删除。

void runq_item_init(runq_item_t *runq_link) {
    dlist_head_init(runq_link);
}

void runq_init(runq_t *queue) {
    dlist_init(queue);
}

void runq_insert(runq_t *queue, struct thread *thread) {
    dlist_add_prev(&thread->sched_attr.runq_link, queue);
}

void runq_remove(runq_t *queue, struct thread *thread) {
    dlist_del(&thread->sched_attr.runq_link);
}

struct thread *runq_extract(runq_t *queue) {
    struct thread *thread;

    thread = dlist_entry(queue->next, struct thread, sched_attr.runq_link);
    runq_remove(queue, thread);

    return thread;
}

调度器

接下来是最有意思的部分-调度器

调度器初始化

调度器工作的第一个阶段是初始化。我们需要为调度器提供一个合适的环境。我们准备一个放置就绪线程的队列,将空闲线程假如队列,开始计数器。计数器将用于统计线程执行的时间片。

调度器初始化代码:

int sched_init(struct thread *idle, struct thread *current) {

    runq_init(&rq.queue);
    rq.lock = SPIN_UNLOCKED;

    sched_wakeup(idle);

    sched_ticker_init();

    return 0;
}

线程的唤醒和执行

从状态机的描述中,我们发现唤醒和开启线程时同样的过程。在调度器初始化时,该函数将被调用;从空闲线程开始执行。那么在唤醒时发生了什么呢?首先,等待的标记被清楚,以为着线程不再处于等待状态。那么线程存在两种可能的状态:休眠或者不休眠。我将在下一部分等待的描述中解释原因。如果线程不休眠,线程仍然处于就绪状态,唤醒完成。不然的话,我们将线程假如调度器队列,溢出等待标记,放入就绪的线程。如果唤醒的线程的优先级比当前的高,那么需要重新安排执行顺序。注意不同的阻塞:所有的操作都在禁止打断的前提下完成。为了掌握线程唤醒的原因,在对称多处理器中,初始化函数将被调用。

/** Locks: IPL, thread. */
static int __sched_wakeup_ready(struct thread *t) {
    int ready;

    spin_protected_if (&rq.lock, (ready = t->ready))
        t->waiting = false;

    return ready;
}

/** Locks: IPL, thread. */
static void __sched_wakeup_waiting(struct thread *t) {
    assert(t && t->waiting);

    spin_lock(&rq.lock);
    __sched_enqueue_set_ready(t);
    __sched_wokenup_clear_waiting(t);
    spin_unlock(&rq.lock);
}


static inline void __sched_wakeup_smp_inactive(struct thread *t) {
    __sched_wakeup_waiting(t);
}

/** Called with IRQs off and thread lock held. */
int __sched_wakeup(struct thread *t) {
    int was_waiting = (t->waiting && t->waiting != TW_SMP_WAKING);

    if (was_waiting)
        if (!__sched_wakeup_ready(t))
                __sched_wakeup_smp_inactive(t);

    return was_waiting;
}

int sched_wakeup(struct thread *t) {
    assert(t);
    return SPIN_IPL_PROTECTED_DO(&t->lock, __sched_wakeup(t));
}

等待

切换至等待模式,然后正确唤醒(当等待事件发生)可能是抢占式调度里最困难的工作。仔细考虑如下的描述:

首先,我们需要向调度器解释我们需要等待某个事件的发生。事件当然不是同步,但是需要将其同步。所以我们应该告诉调度器选择哪个事件的发生。我们不知道它什么时候发生。例如,我们告诉调度器我们等待事件,说明它的出现条件还没有被执行。在这个时候,一个硬件中断发生,它抛出了我们等待的事件。但是当我们准备执行检查时,信息已经丢失。我们通过如下方式解决:

#define SCHED_WAIT_TIMEOUT(cond_expr, timeout) \
    ((cond_expr) ? 0 : ({                                            \
        int __wait_ret = 0;                                          \
        clock_t __wait_timeout = timeout == SCHED_TIMEOUT_INFINITE ? \
            SCHED_TIMEOUT_INFINITE : ms2jiffies(timeout);            \
                                                                     \
        threadsig_lock();                                            \
        do {                                                         \
            sched_wait_prepare();                                    \
                                                                     \
            if (cond_expr)                                           \
                break;                                               \
                                                                     \
            __wait_ret = sched_wait_timeout(__wait_timeout,          \
                                            &__wait_timeout);        \
        } while (!__wait_ret);                                       \
                                                                     \
        sched_wait_cleanup();                                        \
                                                                     \
        threadsig_unlock();                                          \
        __wait_ret;                                                  \
    }))

线程可能处于多个状态叠加后的状态。当其进入休眠后,设置一个额外的等待标记。在唤醒时,该标记被移除;只有当线程到达调度器,离开了激活线程队列,它才会被重新加入。考虑下述图片的场景:

Waiting

  • A: active
  • R: Ready
  • W: wait

用字母代替了状态。浅绿色表示状态处于wait_prepare,绿色表示wait_prepare后,深绿色表示重新安排。如果事件在重新安排前没有发生,线程会继续进入休眠并且等待被唤醒。

Waiting

重新组织

调度器的主要工作是重新组织线程队列。首先,重新组织必须当调度器被阻止后才执行。其次,我们需要指明线程是否允许被抢占。我们将逻辑抽出放置到一个独立函数中,由它调用的块包围和调用,表明在此处我们不允许抢占。

这些动作在就绪线程队列中发生。如果激活线程在重新组织的时刻没有休眠(如果它没有切换至等待状态),我们只是将其添加到调度器的队列中。然后我们从调度器中取出优先级最高的线程。线程的这些规则是先是通过调度策略的帮助实现的。

如果激活线程与从队列中取出的线程时一样的,我们不需要重新组织线程,只需要退出,继续线程执行即可。如果需要重新组织,context_switch和sched_switch将被调用。这些函数中执行调度器需要的步骤。

如果线程进入休眠,切换至等待状态,它将不会进入调度队列,其标记会被清除。最后开始信号处理,但是如上面所述,该内容超越本文主题。

static void sched_switch(struct thread *prev, struct thread *next) {
    sched_prepare_switch(prev, next);

    trace_point(__func__);

    /* Preserve initial semantics of prev/next. */
    cpudata_var(saved_prev) = prev;
    thread_set_current(next);
    context_switch(&prev->context, &next->context);  /* implies cc barrier */
    prev = cpudata_var(saved_prev);

    sched_finish_switch(prev);
}


static void __schedule(int preempt) {
    struct thread *prev, *next;
    ipl_t ipl;

    prev = thread_self();

    assert(!sched_in_interrupt());
    ipl = spin_lock_ipl(&rq.lock);

    if (!preempt && prev->waiting)
        prev->ready = false;
    else
        __sched_enqueue(prev);

    next = runq_extract(&rq.queue);

    spin_unlock(&rq.lock);

    if (prev != next)
        sched_switch(prev, next);

    ipl_restore(ipl);

    assert(thread_self() == prev);

    if (!prev->siglock) {
        thread_signal_handle();
    }
}

void schedule(void) {
    sched_lock();
    __schedule(0);
    sched_unlock();
}

检查多线程操作(ommitted)

<本文完>

]]>
iostream用法 2014-05-12T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/05/12/iostream-summary iostream是C++中一个比较复杂的库,其设计的时候还应用了为人所诟病的钻石继承。在GoNative大会上,有人提问iostream那么复杂,而且相对于C的库函数,其效率相对低,为什么还要将其一直留在标准中。这个问题得到了如下回答(个人总结):

iostream.gif

C++中的iostream是安全的;目前没有库可以取代iostream。

相对于C,iostream做了很多封装,但是其结果是为用户提供类型安全。在C++中cin和cout分别是istream和ostream的实例。

extern istream cin;
typedef basic_istream<char, char_traits<char> > istream;

extern ostream cout;
typedef basic_ostream<char, char_traits<char> > ostream;

通常我们使用的头文件,其中包含如下流对象的定义:

  • cerr/wcerr
  • cin/wcin
  • clog/wclog
  • cout/wcout

由于cin/cout分别是istream和ostream类型的变量,而istream和ostream分别是basic_istream和basic_ostream的typedef,接下来看看basic_istream和basic_ostream中的成员变量和函数:

  1. basic_istream
    • basic_istream构造函数
      cpp explicit basic_istream( basic_streambuf<Elem, Tr> *_Strbuf, bool _Isstd = flase);
    • streamsize gcount() const,返回上一次读入的字节数
    • get,读入一个字符
    • getline,读入一行字符
    • ignore(streamsize _Count=1, int_type _Delim = traits_type::eof());
    • peek,返回下一个将被读取的字符,但是流的位置不前进
    • putback,将一个指定字符放入流中
    • read(char_type *_str, streamsize _count),从流中读入指定长度的字节,存入数组
    • seekg(pos_type _Pos), seeg(off_type _off, ios::base::seekdir _way);
    • sync(),将流与缓冲结合
    • tellg(),返回位置
    • unget(),将最近读取的字符退回流
    • operator>>
  2. basic_stream中的成员
    • basic_stream构造函数
    • flush,刷新缓冲区
    • seekp
    • tellp
    • write(const char_type *_str, streamsize _cnt)
    • operator<<
  3. basic_ios成员
    • basic_ios构造函数
    • bad
    • clear
    • copyfmt
    • eof
    • exceptions
    • fail
    • fill
    • good
    • imbue
    • init, 调用basic_ios构造函数
    • narrow
    • rdbuf
    • rdstate
    • setstate
    • tie: 确保某个流在另一个流之前已经处理
    • widen
    • operator void* 指示流是否仍然处于好的状态
    • operator! 指示流是否不坏

上面了介绍istream和ostream的一些成员函数,其中忽略了一些typedefs。

C++中输入输出流使用cin/cout,其相对于C的scanf和printf更加安全,但是同时也导致其效率相对低。

四个设置函数在头文件<iomanip>中。

  1. 整形数输出格式控制
    • boolalpha, noboolalpha:在输出true/false时,是numeric(0, 1)还是literal(true, false)。
bool is_good = false;
std::cout<<std::boolalpha<<is_good<<std::endl;
std::cout<<std::noboolalpha<<is_good<<std::endl;
  • showbase/noshowbase:显示表示整数的基,0X,0等。
  • hex/oct/dec:显示输出的进制。 16进制中x的大小写,uppercase/lowercase
int a = 0x36;
std::cout<<std::dec<<a<<std::oct<<a<<std::hex<<a<<std::endl;

以上功能改变都是持续性的,直到改变回原样为止。

  1. 浮点数格式化输出
    • setw(int), setprecision(int), setfill(char), setbase(8/10/16):这三个函数分别是设置输出的宽度,精度以及当宽度不足时的补充和输出的基数。
    • showpoint/noshowpoint:设置输出小数位是0的浮点数时是否强制显示小数位的0。
    • left/right/internal:设置在setw后当宽度小于设置的值时的填充位置。internal输出有符号数时,左移符号位,右移数字,在中间填充字符。
    • cout.unsetf(ostream::floatfield):恢复到初始状态,有效数字为6位。
    • fixed/scientific:fixed固定输出小数点后6位
#include <iostream>
#include <iomanip>

int main()
{
    std::ios::fmtflags flags(std::cout.flags());
    int a = 3;
    // fill 0, default: fill '', right align;
    std::cout<<std::left<<std::setw(3)<<std::setfill('x')<<a<<std::endl;
    double b = 3.123;
    // output two effective numbers after decimal point
    std::cout<<std::fixed<<std::setprecision(2)<<b<<std::endl; 
    std::cout.flags(flags);
    // output 2 effective number
    std::cout<<std::setprecision(2)<<b<<std::endl; 
}

会导致流刷新的事件:

  • 缓冲区满。
  • 在输出流之后有输入操作,立即刷新缓冲区
  • 使用相关运算子或者函数主动刷新流。
    • 使用 endl 操控器的输出流中插入一个换行符,并刷新缓冲区。与 endl 的操控,使用插入运算符,如下所示: cout << ... << endl;
    • 使用 ostream 类或刷新操控器中的刷新成员函数。齐平的操控程序不会流到插入换行符之前它刷新缓冲区。要调用的齐平的成员函数,请使用类似于以下代码: cout.flush();
    • 齐平的操控与使用插入运算符,如下所示: cout << ... << flush;
    • 从 cin 流中读取或写入 cerr 或 clog 的流。由于这些对象会与 cout 共享缓冲区,每对它进行任何更改之前刷新缓冲区的内容。
    • 退出该程序以刷新当前正在使用的所有缓冲区。

参考文献:

  1. MSDN iostream
  2. cplusplus.com,继承图出处

<本文完>

]]>
C++中动态内存分配 2014-05-04T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/05/04/operator-new-delete 在C++语言中,总共有5种内存分配区域:栈,堆(heap),自由存储区(free store),常量存储区,全局(静态)存储区。其中堆是用malloc/free一对系统库函数来管理动态内存的申请和释放,自由存储区是用new/delete一对操作符来动态管理内存的分配和释放。new/delete在标准中可以使用malloc/free实现,但是malloc/free不能使用new/delete实现。

他们之间的区别:

  • malloc/free是cstdlib中的函数,new/delete是操作符。
  • malloc/free只是分配所需的空间,若分配空间失败则返回空指针;new/delete不仅会分配空间,也会调用想用的构造函数和析构函数。其失败可以返回空指针,也可以抛出bad_alloc的异常,或者调用相应的handler,其函数声明是void(*)(void),在new头文件中,通过set_new_handler来设置,其返回之前的处理,一般都是调用terminate(),结束程序的执行。

在C++中系统提供了几种全局的函数:

  • void * operator new(size_t sz)
  • void * operator new(size_t sz, std::nothrow_t&);
  • void * operator new(size_t sz, void *ptr);
  • void * operator new(size_t sz,/* args... */); // class specific

上面三个函数分别表示默认的new操作符,不抛出异常的new操作符,placement new操作符。

我们能重载的只能是在class specific的new构造函数,(上面给出new的版本,没有给出new[],以及delete,delete[]版本)。

默认的delete版本:

  • void operator delete(void *ptr);

在重载的时候,如果重载其中一个版本,则其它版本需要一同实现,以保证安全。

#include <iostream>

using namespace std;

class A
{
public:
    static void* operator new(std::size_t sz)
    {
        std::cout<<"New"<<std::endl;
        return ::operator new(sz);
    }

    static void* operator new(std::size_t sz, const std::nothrow_t&)
    {
        std::cout<<"Nothrow:"<<std::endl;
        return ::operator new(sz);
    }

    static void operator delete(void *ptr)
    {
        std::cout<<"Delete"<<std::endl;
        ::operator delete(ptr);
    }

    static void operator delete(void *ptr, const std::nothrow_t &);

};

int main()
{
    // std::nothrow是提供的默认值
    // 调用不抛出异常的版本
    A *a = new(std::nothrow) A;
    delete a;

    a = new A; // 调用重载的版本
    delete a;

    void *ptr = ::operator new(sizeof(A));
    a = new(ptr) A; // placement new, 是系统默认的。
    a->~A(); // 需要主动调用析构函数,收回空间
    delete a;

    return 0;
}

上面是简易版本的重载。需要注意的是,重载时必须其函数是静态函数,函数的声明除了必须的函数参数,其它参数都是可选的。在重载的时候需要提供其它参数,就如上面代码中的std::nothrow。

  1. Cpp Reference operator new
  2. Cpp Reference Operator delete

<本文完>

]]>
C++中模板的学习 2014-04-27T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/04/27/template-in-cpp C++中的模板是C++中实现元编程的一种主要途径。元编程,即编写程序的程序。维基百科上说:

Metaprogramming is the writing of computer programs that write or manipulate other programs (or themselves) as their data, or that do part of the work at compile time that would otherwise be done at runtime. In some cases, this allows programmers to minimize the number of lines of code to express a solution (hence reducing development time), or it gives programs greater flexibility to efficiently handle new situations without recompilation.

维基百科上说是在编译器间完成,其实脚本像Ruby可以在运行期间可变程序的行为。首先来看看tempalte的一些基本应用。

  • 函数模板
template<typename T1, typename T2>
std::common_type<T1,T2>::type add(T1 t1, T2 t2);

上述是声明一个函数模板,其可以针对两种类型相加,然后给出结果。commontype是c++11中在typetraits中引入的。如果T1和T2没有公共类型就会出错。

  • 类模板
template<typename T>
class Stack
{
private:
    T value;
    //...other stuff...
};

上述是声明一个简单的Stack类,其中模板参数表示其可以存储任何类型。

一般主要就是用于以上两个目的:函数模板和类模板。函数模板和类模板是不一样的,类模板有特化,偏特化和完全特化;函数模板没有,函数模板只有重载。看看下面一个例子:

// 1
template<typename T>
void f(T);

// 2
template<>
void f<int*>(int*);

// 3
template<typename T>
void f(T*);

int a;
int *b = &a;
f(b)

上述的f函数调用哪一个函数呢?绝大多数人凭着自己直觉会觉得肯定调用2,因为这个是最特殊的。但是其实不是,它调用第三个。(为了能够调用,大家可以给他们分别加上函数体,打印相关东西)

函数调用的顺序如下:

  • plain old function: 没有模板修饰的,匹配的优先级最高。
  • primary template overload: 稍微特化一点的模板。
  • primary template: 最开始的模板。

上述函数调用使用的是primary template overload这种调用方式。第一个模板实际上是T->T的形式,而第三个模板是T->T*的形式。上面是函数模板与类模板一个重要的区别,需要仔细体会。

函数模板的功能很大,看看如何来发挥一下它。首先打印一个10!,在编译期间完成计算。

template<int n>
struct Factor
{
    const static int Value = Factor<n-1>::Value * n;
};

template<>
struct Factor<1>
{
    const static int Value = 1;
};

cout<<Factor<10>::Value<<endl;

当然这种方式必须给n一个定值,否则就会编译出错。在C++11中还引入可变模板参数。

template<typename T, typename...Args>
void print(T t, Args...args)
{
    cout<<t<<endl;
    print(args...);
}

上述是可变模板参数编写的print函数,我们可以给print传入类型的值。

模板还可以为我们带来一些比较奇怪的好处:帮助我们访问类的私有变量。

class Earth
{
private:
    int private_;
public:
    template<typename T>
    void f(T t);
    void g();
};

上面是一个类的定义,我们可以通过哪些方式访问其私有变量private_呢?

  • 给此类添加一个公有函数
  • 利用内存布局的知识,将其重新解释reinterpret_cast(&earth_obj)
  • 添加友元函数
  • 给类增加一个模板函数:Bingo.
struct Y{};
template<>
void Earth::f<Y>(Y y) {/* hacker's laugh */};

接下来研究一下《More Exceptional C++》(Herb sutter)上的一个例子。

template<typename T1, typename T2>
void g(T1, T2);
template<typename T> void g(T);
template<typename T> void g(T,T);
template<typename T> void g(T*);
template<typename T> void g(T*,T);
template<typename T> void g(T, T*);
template<typename T> void g(int, T*);
template<> void g<int>(int);
void g(int, double);
void g(int);

int i;
double d;
float f;
complex<double> c;

// test cases
g(i);         // 1
g<int>(i);    // 2
g(i,i);       // 3
g(c);         // 4
g(i,f);       // 5
g(i,d);       // 6
g(c, &c);     // 7
g(i, &d);     // 8
g(&d, d);     // 9
g(&d);        // 10
g(d, *i);     // 11
g(&i,&i);     // 12

上面提到函数模板只有重载,而没有偏特化这种形式,再强调一次,在匹配时,

  • 自由函数,函数参数类型必须完全一致,不允许转换
  • 函数模板->最特化的模板
  • 函数模板->通用模板

匹配结果如下:

  • 第一个函数匹配:g(int);
  • 第二个匹配:template<> void g(int);
  • 第三个匹配:template void g(T,T);
  • 第四个匹配:template void g(T);
  • 第五个匹配:template void g(T1, T2);
  • 第六个匹配:void g(int, double);
  • 第七个匹配:template void g(T, T*);
  • 第八个匹配:template void g(int, T*);
  • 第九个匹配:template void g(T*,T);
  • 第十个匹配:template void g(T*);
  • 第十一个匹配:template void g(T1, T2);
  • 第十二个匹配:template void g(T,T);

总结一下:C++中的模板帮助我们编写通用的函数和类,而且也提供了元编程的能力。但是每一技术都需要仔细去研究才会得心应手。

<本文完>

]]>
sizeof的总结 2014-04-24T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/04/24/sizeof-summary C/C++,Java中都有字节对齐的概念。字节对齐的目的是加快CPU的访问速度,因为将字节对齐的相应长度的整数倍位置上,CPU取数据会更快。为什么呢?

Cache可以看成一些可以用非常快的速度进行访问的临时内存。但是Cache的容量不大,比如一般一级Cache只有几K到几十K,二级Cache只有几百K到几M.这个同数G的内存相比,是比较小的。但是CPU访问内存非常慢,所以硬件会将平时经常使用的内容存放到Cache里面。Cache是通过一些Cache Line来组织的,每一条Cache Line一般包含16个字节,32个字节或64个字节等。 比如某个计算机一级Cache的Cache Line长度是32个字节,那么每段Cache Line总是会包含32个字节对齐的一段内存。现在有一个4字节的整数,如果它的地址不是4字节对齐的,那么就有可能访问它的时候,需要使用两条Cache Line,这增加了总线通讯量,而且增加了对Cache的使用量,而且使用的数据没有在Cache里面(这时需要将数据从内存调入Cache,会非常慢)的机会会增加,这些都降低了程序的速度。参考

C/C++中可以使用sizeof来获取一个结构或者类所占的空间大小,它有三种用法:

  • sizeof(obj)
  • sizeof obj
  • sizeof(Type)
  • sizeof Type // error

其中上面第四种形式表示不能直接在类型上不加括号的使用sizeof运算符。sizeof是运算符,它返回其输入的字节数。而且sizeof的值是在编译期间决定的,编译器可以选择进一步优化。

    const char *dog_name = "Lucky";
    int dogs[20];
    int *dog_ptr = dogs;
    const char *dog_name_ptr;
    void (*f)(int,int);

分别对上述4个变量做sizeof运算符,在32位机器上,会得到如下结果:6,80,4,4,4。在32位机器上指针都是4个字节长度。但是对于像dogs和dog_name为什么会返回正确的结果呢?因为它们其实也是指针。但是编译器知道我们访问的具体是什么,所以会替我们算出正确的结果。

    auto fun_ptr = [](const char*lhs) { cout<<sizeof(lhs)<<endl;}
    auto fun_arr = [](int lhs[]) { cout<<sizeof(lhs)<<endl;}

如果我们将上面的dog_name和dogs分别传入上面的函数(上面我用lambda函数写的,为了方便),打印的结果都是4(32位机器)。这里就会涉及到数组指针的退化问题:函数传递以及[]解引用操作符都会引起退化,退化成一个指针,对这些值取sizeof都会得到相应机器上指针的字节数。

接下来,讨论一下字节对齐的问题。上面介绍过字节对齐的用户,下面直接讨论如何分析一个结构体的所占的大小。

    struct MyStruct { int a; int b;};
    (int)(&((MyStruct*)0)->a); // 这个代码是计算相对偏移量
    offsetof(MyStruct,a); // cstddef头文件中包含这个定义

上面自己写的代码还是利用系统提供的代码都可以计算某个变量在结构体中的偏移量,这样就可以查看编译器会结构体做字节对齐时,补了多少字节。首先给出一个字节对齐的公式:

结构体字节数 = 最后一个变量的偏移量 + 最后一个变量的大小 + 尾部填充字节

结构体中的每个变量的便宜地址都必须是其字节的整数倍:char(1), short(2),int(4),long(4),float(4),double(8),如果没有用宏#pragma pack n,设置对齐要求的话,每个都按自己要求对齐,在设置了以后按照二者的最小值来对齐。每个变量完成字节填充后,都对齐至所要求的位置,然后对整个结构体要求是:结构体的大小必须是结构体内最大对齐数的整数倍。下面举两个例子分别说明。

    // example 1
    struct Test1
    {
        char b;
        int a;
    };

    // example 2
    struct Test2
    {
        int a;
        char b;
        char c;
    };

对上述两个结构体分别作sizeof运算会得到:8,8。char对齐要求偏移量是1的整数倍,int要求是4的整数倍。对于Test1,b的偏移量是0,满足;如果不填充字节,那么int是1,不满足,所以在char后填充3个字节,此时int偏移量是4,结构体总字节是8。

在Test2中,a的偏移量是0,满足;b的偏移量是4,c的偏移量是5,总字节是6。但是6不是最宽类型int的4的整数倍,所以末尾还要增加2个字节作为对齐,所以其字节数时8。在Test1中,我们并未分析整个总字节数问题,因为其结果正好符合要求。

    // example3
    struct Inner
    {
        double a;
        int b;
    };
    struct Test3
    {
        int a;
        double b;
        char c;
        Inner d;
        char e;
    };

对Test3去sizeof结果是:48。我特地去取了一非常复杂的表达式,综合了内部结构体,字节填充,尾部填充。Inner字节对齐后的结果是16,其最大的字节对齐是8,其必须在8的位置上(即使我们将int和double的位置交换,Inner在Test3内部的对齐也必须是以8的倍数)。

  • int a, 其偏移量为0,但是后面是double,对齐为8的倍数,所以其补3字节:8
  • double b:8
  • char c;因为其后面是按8的倍数对齐,所以其要补7个字节:8
  • Innder d:16
  • char e:原本不需要填充,那么Test3的大小为41,不是最大的8的倍数,所以补充7个字节,所以总字节数是48。

sizeof在结构体上的运算结果就只能帮你到这了。接下来看看sizeof在class上的结果。

    // example 4
    class Test4
    {
        ;
    };
    class Test5
    {
        int a;
        char c;
    };
    class Test6
    {
        char c;
        int a;
    public:
        void f() {cout<<"Hi"<<endl;}
    };

    class Test7
    {
        char c;
        static char b;
        int a;
    public:
        void f() {cout<<"Hi"<<endl;}
    };

上面给出了一连串的类,每个的字节大小其实都很容易算,规则如下:

  • 按照struct相同的方式算字节填充
  • 非虚函数定义不会占用空间
  • 静态成员不会占用空间(全局静态存储区)
  • 空的类至少有一个字节,因为为了保证每个变量的地址唯一,否则这个空类前后的地址就重复

答案留给大家自己去运算和跑吧。

下面说一下当存在类的继承和虚函数时sizeof是如何给出结果的。(在测试的时候需要一个比较大的坑,在线编译器的指针大小是8字节,导致以为自己的分析错了)其基本规则如下:

  • 如果类里面有虚函数那么其大小至少是sizeof(void*)
  • 如果类从其它类派生,需要包括其它类的成员变量大小,如果只有一个基类,基类中有虚函数的话,子类中的虚函数就加入这个虚表,在类中保存这个指针。
  • 当从多个基类派生时,当不同基类中有虚函数时,要增加多个指针,指向多个不同的虚表

P.S. 如果sizeof(void*)大小的虚函数也要参与字节对齐过程。4字节int和8字节的虚表指针会导致这个类的大小为16。具体的例子大家参考参考文献。

    // example 5
    class Base1
    {
        int a;
    public:
        virtual void fun1() {};
    };

    class Derived: public Base1
    {
    public:
        virtual void fun2(){}
    };

    class Base2
    {
    public:
        virtual void fun3(){};
    };

    class Derived1:public Base1, Base2
    {
    public:
        virtual void fun4(){}
    };

    // 菱形继承
    class Base3:public Base1
    {
    public:
        virtual void fun5(){}
    };
    class Base4:pulbic Base2
    {
    public:
        virtual void fun6(){}
    };
    class Derived2:public Base3, Base4
    {
    public:
        virtual void fun7(){}
    };

    // 虚继承
    class Base5:public virtual Base1
    {
    public:
        virtual void fun8(){}
    };
    class Base6:public virtual Base1
    {
    public:
        virtual void fun9(){}
    };
    class Derived3: public Base5, Base6
    {
    public:
        virtual void fun9(){}
    };

上面的测试范例综合包括所有虚函数的场景。在32位机器上,虚函数的指针为4字节。

  • Base1: 8字节,int 4字节,虚函数指针4字节 = 8字节
  • Derived1: 继承自Base1, 4字节指针,继承成员变量4字节
  • Base2:只有虚函数指针,4字节
  • Derived2: 两个虚表 4+4,加两个成员4, 16
  • Derived3: 三个虚表,加一个成员变量,16

以上的分析不知道是否是正确的,在参考文中分析菱形继承和虚拟继承时并没有考虑基类也具有虚函数的场景。但是通过测试发现如下现象:

  • 当采用虚拟继承时,基类(若基类有虚函数)的虚表指针也会在派生类中出现,即派生类会增加一个虚表指针的大小
  • 当不采用虚拟继承时,只需要关心派生类的基类的情况,以及基类的成员情况,不用增加一个额外的指针。即上面的Derived3中,如果Base1中没有虚函数,那么这个大小就是12字节。Derived有两个虚表指针,加上一个(由于是虚拟继承,只有一份)整型变量。

参考:

<本文完>

]]>
Remove comments in java or C++ 2014-04-23T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/04/23/thoughts-of-recent-life @(Technology)[life|thoughts]

近段时间其实也没有做什么具体的事,只是抽了很多时间看了很多书。虽然自己也很紧张,但是在这种忙里偷闲的时间里也不忘学学脚本。

这段时间在Code Academy上学习了Ruby和Python。最开始的第一印象是:为什么Python没有对类中的成员变量或者成员函数进行一些作用域的设定?

Ruby给出非常直接的解决方法,public,private,protected关键字来支持,同时也支持一些例如@name,@@name,$name这种方式来区分不同的变量归属。上句中三个分别指代类实例变量,类变量和全局作用域变量。

之后从图书馆借了两本书:《Ruby元编程》和《Ruby编程语言》。前者是讲述Ruby在元编程领域的各种奇淫技巧,后者是讲述Ruby的基本语法知识。由于第一本书比较薄,所以一开始就尝试将这本书每天看一章的方式进行阅读,当然书的作者也是如此安排其内容。看了第一章以后就觉得:Ruby很神奇!在后续的章节阅读中,越发觉得Ruby的设计思想就是简洁,它提供给用户最大的自由度。当然这种自由度是一把双刃剑。例如本来关键字private是禁止访问类的私有成员函数的,但是通过:send方法以及instance_eval这种作用域控制的符号,你可以访问。

Ruby是一种动态脚本,它相对于静态的编译语言忽略了类型信息,但是不代表它没有类型。Ruby中几乎一切都是Object,它的继承体系可以得出:Object<-Module<-Kernel<-Class。而Object本身又是Class的子类。类的名字其实是另外一种对象而已。在Ruby1.9中,又引入了BasicObject。因为方法太多,BasicObject仅保存了很少的函数。方便用户继承,而不用引入非常多的方法。

Ruby中提供最方便的东西Integer, String, Hash, Array。这些都是一般脚本都会提供的类型,而且基本都做得很强大。Ruby支持大数运算。

Ruby允许对一个已经定义好的类动态的添加方法,动态的删除方法;Ruby允许给一个对象定义方法,这个方法称为单件方法(与设计模式中的单件不一样);Ruby可以对方法起别名,环回别名可以在版本升级时减少代码量;Ruby可以当成一种领域特定语言来使用,这也是别名的功能之一。

Ruby提供的功能非常的强大,只是觉得用户的自由非常充分,唯一剩下的就是自己如何发挥这种自由度,创造出功能比较强大的模块或者类。在Ruby中对某个问题总有解决方法,而且绝对不只一种方法。

代码的本领是练出来的,后来codekata上看到了一个练习:删除Java中的注释。这个看似简单的任务其实存在很多的坑在里面。Ruby的String类对正则表达式的支持比较强大,我选择使用正则来处理。大概的思路是一行一行的处理,针对每一行删除行注释,块注释,并且需要注意多行的注释以及隐藏在代码中间的注释。

  1. 第一版本的思路
    利用两个正则表达式来删除:
    • “/\/*.*\*\//”,删除/* */中间的注释。
    • /\/\/.*/ ,删除块注释。
      这个时候只能针对比较简单的注释进行删除,当然我的程序中已经增加如何处理多行注释的功能。
  2. 第二版本的思路
    意识到第一版本的存在问题太多:对于冒号中的注释不应该删除,因为这是字符串内容;多行注释的时候(.*)会进行贪婪的匹配,这样对于/*xxx*/xxx/*xxx*/会出现这样的问题,中间都被删除了。
  3. 第三版本的思路
    第三版本的思路主要是在前两个问题的基础上解决的。第一、针对贪婪的方式,经过调研以后发现在匹配后面增加?,原来的模式就变为非贪婪模式;第二、针对这种双引号中的内容问题,采取迂回战术,首先,将双引号之间的内容匹配,然后将双引号中的/*, */和//都替换成一个已知的常量。然后再进行正常的删除,在删除完以后再将所有的替换后的东西换成之前的内容。

一个注释删除程序检测的能力超过我的想象,不仅仅是Ruby基本的代码能力,还包括:正则表达式,Ruby中的块传递返回值问题,注释中的各种extreme case识别,程序的逻辑,调试能力。

万里之行始于足下。

<本文完>

]]>
Closures in Ruby 2014-04-18T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/04/18/closures-in-ruby Closures in Ruby

@(Technology)[Ruby|Closure]

这篇文章是翻译自:Paul Cantrell

我推荐先运行这个文件,然后一边读,一边观察其后的结果。当然你也可也先删除所有的注释,猜测所有程序的结果,以此来测试你自己Ruby能力。

一个闭包是满足如下三个准则:

  • 可以当做一个变量(value)来传递
  • 任何人只要具有那个变量的引用,在任何时候都可以执行
  • 它可以保存它创建时的上下文中的变量(它对于其封存的变量的访问是禁止的,这是其closure给人的感觉)。

closure这个名词的含义各家各持己见,一些人认为准则中不应该包括一,但是我认为应该是。

闭包是函数式编程中的主要概念,但是在其它语言中也支持(如Java的匿名内部类)。有了闭包,你可以做一些很炫的事:他们允许延迟调用(deferred execution)以及一些非常优雅的技巧。

Ruby是基于“最小惊异原则”而设计的,但是在学习时,我却有一些不愉快的惊异。当我明白想"each"这种方式的原理时,我想,“啊哈,Ruby有闭包”。但是我却发现函数不能同时接受多个块(block)---这违反了闭包可以像变量一样自由传递的原则。

这篇文档详细记录我在搞清楚Ruby中闭包到底是如何工作时的所学到的知识。

def example(num)
    puts
    puts "-------- Example #{num}---------"
end

第一部分:块

  1. 块就像闭包一样,因为他们可以引用他们定义处的变量。
 1 # example 1
 2 def thrice
 3     yield
 4     yield
 5     yield
 6 end
 7 x = 5
 8 puts "value of x before: #{x}"
 9 thrice{x += 1}
10 puts "value of x after: #{x}"
  1. 一个块可以引用它定义处的变量,而不是其调用处的变量
# example 2
def thrice_with_local_x
    x = 100
    yield
    yield
    yield
    puts "value of x at the end of thrice_with_local: #{x}"
end

x = 5
thrice_with_local_x {x += 1}
puts "value of outer x after: #{x}"
  1. 一个块只能引用在创建上下文中已经存在的变量,如果他们不存在,块不会创建他们(译者注:即这个变量不会再块创建的上下文中存在)
# example 3
thrice do
    y = 10
    puts "Is y defined inside the block where it is first set?"
    puts "Yes." if defined? y
end
puts "Is y defined in the outer context after being set in the block?"
puts "NO!" unless defined? y
  1. 目前为止,块似乎和闭包一样:他们对所创建处的变量访问是封闭的,而与他们调用处的上下文无关。但是就目前我们的使用方法来看,他们不是不全是闭包,因为我们无法传递他们。yield只能指代传递入相应方法的block。但是我们可以使用&符号,来继续传递块。
# example 4
def six_times(&block)
    thrice(&block)
    thrice(&block)
end

x = 4
six_times { x += 10}
puts "value of x after: #{x}"
  1. 现在我们具有闭包了吗?不完全是!我们不能保存一个&block,然后延迟到任何一个时间来调用它。如下代码不能编译:
def save_block_for_later(&block)
    save = &block
end

但是我们可以把&符号丢掉,这样就可以传递了。

# example 5
def save_block_for_later(&block)
    @save = block
end
save_for_later{puts "Hello"}
puts "Deferred execution of a block:"
@saved.call
@saved.call

但是,等等!我们不能给一个函数同时传递多个块。结果是,一个函数至多有一个块,而且&block必须是最后一个参数。

# def f(&block1, &block2)
# def f(&block1, arg_after_block)
# f {puts "block1"} {puts "block2}

到底是怎么回事?我觉得这种单个块的限制违反了最小惊异原则,原因是C实现的难易程度,而不是语法问题。所以:现在我们用闭包任何健壮以及有兴趣的事的想法被毁了吗?

Ruby中类似闭包的构建

  1. 事实上想法还在。当我们利用&block传递块时,他们指向那个没有&的参数,他们是Proc.new(&param)的简写
# example 6
def save_for_later(&b)
    @saved = Proc.new(&b)
end

save_for_later{puts "Hello again!"}
puts "Deferred execution of a Proc works just the same with Proc.new"
@saved.call

利用Proc,我们随时定义块,不用&参数。

# example 7
@saved_proc_new = Proc.new {puts "I'm declared on the spot with Proc.new"
puts "Deferred execution of a Proc works just the same with ad-hoc Proc.new"
@saved_pro_new.call

hold住。纯闭包。但是等一等,还有更多的。Ruby一堆类似闭包的东西,可以.call的方式来调用。

@saved_proc_new = Proc.new {puts "I am declared with Proc.new"}
@saved_prc = proc {puts "I am delcared with proc"
@saved_lambda = lambda {puts "I am declared with lambda"}
def some_method
    puts "I am declared as a method"
end
@method_as_closure = method(:some_method)
puts "Here are four superficially identical forms of deferred execution:"
@saved_proc_new.call
@saved_proc.call
@saved_lambda.call
@method_as_closure.call

其实事实上,至少有7中方法 + block(implicitly passed, called with yield) + block(&b, f(&b) yield) + block(&b, b.call) + Proc.new + proc + lambda + method

尽管他们长相各异,但是其中一些事等价的。其中1和2不是真正的闭包,实际上他们是同样的东西。3-7看起来是一样的。但是他们只是语法不同还是语义上完全一样呢?

第三部分:闭包和控制流

他们不一样,其中一个很明显的不同是他们对return语句的处理。在如下没有return语句的不同像闭包一样的东西中,他们表现方式完全一样。

# example 9
def f(closure)
    puts
    puts "about to call closure"
    result = closure.call
    puts "closure returned: #{result}"
    "value from f"
end

puts "f returned: " + f.(Proc.new {"value from Proc.new})
puts "f returned: " + f.(proc {"value from proc})
puts "f returned: " + f.(lambda {"value from lambda})
def another_method
    "value from another_method"
end
puts "f returned: " + f.(method(:another_method))

但是一旦有return,好像一切都被打松散了。

# example 10
begin
    f(Proc.new {return "value from Proc.new"))
rescue Exception => e
    puts "Failed with #{e.class}: #{e}"
end

上述调用会失败,因为return语句必须在一个函数里面调用,但是Proc不是实际上全功能的函数。

# example 11
def g
    result = f(Proc.new {return "Value from Proc.new"})
    puts "f returned: " + result # never executed
    "value from g"               # never executed
end
puts "g returned: #{g}"

注意Proc.new中的return不仅仅从Proc中返回,直接从g中返回,不仅跳过g后续的语句,而且f后续语句也被跳过,像异常一样。这意味着当创建的上下文不存在时,调用一个带return的Proc是不可能的。

# example 12
def make_proc_new
    begin
        Proc.new { return "Value from Proc.new"}
    ensure
        puts "make_proc_new exited"
    end
end

begin
    puts make_proc_new.call
rescue Exception => e
    puts "Failed with #{e.class}: #{e}
end

上述方法使得在多个线程间传递Procs不安全。一个Proc.new不是真正封闭:它取决于创建上下文是否是存在。因为return和那个上下文是绑定的。目前lambda不是这么表现的。

# example 13
def g
    result = f(lambda {return "Value from lambda"})
    puts "f returned: " + result
    "Value from g"
end
puts "g returned: #{g}"

你可以调用一个lambda,尽管创建的上下文已经不存在。

# example 14
def make_lambda
    begin
        lambda { return "value from lambda"}
    ensure
        puts "make_lambda exited"
    end
end

puts make_lambda.call

lambda中的return语句只是从lambda块中返回,流控制正常进行。所以lambda就像一个函数一样,Proc与其调用者的控制流程是独立的。lambda是Ruby中的真正的闭包。proc是Proc.new的简写。

def g
    result = f(proc {return "value from proc"})
    puts "f returned: " + result
    "Value from g"
end
puts "g returned: #{g}"

在Ruby1.8中,它是lambda的简写,在Ruby1.9中它是lambda的简写。

"return",从调用者返回:

  • block(called with yield)
  • block(&b => f(&b) => yield)
  • block(&b => b.call)
  • Proc.new
  • proc in 1.9

"return",仅仅从闭包中返回

  • pric in 1.8
  • lambda
  • mthod

第四部分:闭包与参数个数

不同Ruby闭包中的另外一个不同点是他们如何处理不匹配的参数-参数个数不匹配。闭包除了call方法,还有一个arity方法,返回其想要的参数个数

# example 16
puts "One-arg lambda: "
puts (lambda{|x|}.arity)
puts "Three-arg lambda: "
puts (lambda{|x,y,z|}.arity)

puts "No-args lambda: "
puts (lambda{}.arity) # about to change
puts "Varargs lambda: "
puts (lambda{|*args|}.arity)
# example 17
def call_with_too_many_args(closure)
    begin
        puts "closure arity: #{closure.arity}"
        closure.call(1,2,3,3,4,6)
        puts "too many args worked"
    rescue Exception => e
        puts "too many args threw exception #{e.class}"
    end
end

def two_arg_method(x,y)
end

puts; puts "Proc.new:"; call_with_too_many_args(Proc.new {|x,y|})
puts; puts "proc:"    ; call_with_too_many_args(proc {|x,y|})
puts; puts "lambda:"  ; call_with_too_many_args(lambda {|x,y|})
puts; puts "Method:"  ; call_with_too_many_args(method(:two_arg_method))

def call_with_too_few_args(closure)
 begin
    puts "closure arity: #{closure.arity}"
    closure.call()
    puts "Too few args worked"
 rescue Exception => e
    puts "Too few args threw exception #{e.class}: #{e}"
 end
end

puts; puts "Proc.new:"; call_with_too_few_args(Proc.new {|x,y|})
puts; puts "proc:"    ; call_with_too_few_args(proc {|x,y|})
puts; puts "lambda:"  ; call_with_too_few_args(lambda {|x,y|})
puts; puts "Method:"  ; call_with_too_few_args(method(:two_arg_method))

# Yet oddly, the behavior for one-argument closures is different....

# example 18

def one_arg_method(x)
end

puts; puts "Proc.new:"; call_with_too_many_args(Proc.new {|x|})
puts; puts "proc:"    ; call_with_too_many_args(proc {|x|})
puts; puts "lambda:"  ; call_with_too_many_args(lambda {|x|})
puts; puts "Method:"  ; call_with_too_many_args(method(:one_arg_method))
puts; puts "Proc.new:"; call_with_too_few_args(Proc.new {|x|})
puts; puts "proc:"    ; call_with_too_few_args(proc {|x|})
puts; puts "lambda:"  ; call_with_too_few_args(lambda {|x|})
puts; puts "Method:"  ; call_with_too_few_args(method(:one_arg_method))

# Yet when there are no args...

#example 19

def no_arg_method
end

puts; puts "Proc.new:"; call_with_too_many_args(Proc.new {||})
puts; puts "proc:"    ; call_with_too_many_args(proc {||})
puts; puts "lambda:"  ; call_with_too_many_args(lambda {||})
puts; puts "Method:"  ; call_with_too_many_args(method(:no_arg_method))

Ruby中Proc.new, proc, lambda将一个参数作为一个特殊情况,表现不一致,但是method是一致的。(译者注:在Ruby2.0中行为一致,即Proc.new/proc全适应,lambda/method,全不适应,抛出异常)

第五部分:责骂

这是一个比较令人眩晕的语法选项,具有一些不是十分清楚的细微语法区别,而且在特殊情况下表现不同。程序员希望语言能工作,就像一个捉大熊的陷阱。

为甚事情会这样?因为Ruby:

  • 由实现设计,并且
  • 受实现约束。

语言一直在发展,因为Ruby小组一直有好玩的想法,但是没有除了CRuby没有维护一个实际的说明书。一个将语言的逻辑结构表述清楚,进而帮助支出我们刚才所见的不一致性。相反,这种不一致性已经渗入语言,把像我这样想学这种语言的人搞得晕头转向,然后以为是bug就提交上去了。向上帝发誓,类似proc这种基本语义的东西不应该是一团糟,以至于不得不在版本之间回溯。是的,我知道,设计语言很难,但是像proc/lambda对arity这种问题第一次时容易解决。抱怨,抱怨。

第六部分:总结

到现在为止,对于创建闭包的7种方法,我们发现了什么东西:

name | True closure | Return | Arith check ----- | ------------ |---------| ----------- block (called with yield)| N|declaring|N block (&b => f(&b) => yield)|N|declaring|N block (&b => b.call)| Y except return|declaring|N Proc.new | Y except return|declaring |N proc | alias for lambda in 1.8 / Proc.new in 1.9| lambda | Y | closure | yes, except 1 method | Y | closure | y

下面每组在语义上相同,只是在语法上有区别:

  • block (called with yield)
  • block (&b => f(&b) => yield)

  • block (&b => b.call)
  • Proc.new
  • proc in 1.9

  • proc in 1.8
  • lambda

  • method

或者至少是我基于实验给出的观点。除了测试CRuby的实现,没有其他权威的回答。因为根本没有说明书,所以可能还有其他我没有发现的区别。到此为止,“Ruby makes Paul carzy”告一段落。从这里开始,将是一个特别棒的部分。

第七部分:用闭包做一些非常炫的事

让我们一起写一个包含所有Fibonacci数的数据结构。是的,我说的是全部。这个可能吗?我们将使用闭包来实现懒惰评估,所以电脑只计算我们要它做的。

为了完成这个工作,我们将使用想Lisp一样的链表:一个递归式的数据结构,包括两个部分:car,链表下一个元素;cdr,链表其余部分。

例如,前三个数的链表是[1,[2,[3]]]。为什么呢?因为

  • [1,[2,[3] <--- car = 1, cdr = [2,[3]]
  • [2,[3] <--- car = 2, cdr = [3]
  • [3] <--- car = 3, cdr = nil

下面是遍历这种链表的类。

# example 20
class LispyEnumerable
  include Enumerable

  def initialize(tree)
    @tree = tree
  end

  def each
    while @tree
      car,cdr = @tree
      yield car
      @tree = cdr
    end
  end
end

list = [1,[2,[3]]]
LispyEnumerable.new(list).each do |x|
  puts x
end

现在我们如何制造无穷长度链表?为了取代构建一个完整的数据结构的中的每一个节点,我们使用闭包。直到我们需要值的时候,我们才会去调用闭包。这种调用是递归的:树的顶端是个闭包,它的cdr也是闭包,cdr的cdr也是闭包。

# example 21
class LazyLispyEnumerable
  include Enumerable

  def initialize(tree)
      @tree = tree
  end

  def each
      while @tree
          car,cdr = @tree.call # <--- @tree is a closure
          yield car
          @tree = cdr
      end
  end
end

list = lambda{[1, lambda {[2, lambda {[3]}]}]} # same as above, except we wrap each level in a lambda
LazyLispyEnumerable.new(list).each do |x|
  puts x
end

# example 22

# Let's see when each of those blocks gets called:
list = lambda do
  puts "first lambda called"
  [1, lambda do
    puts "second lambda called"
    [2, lambda do
      puts "third lambda called"
      [3]
    end]
  end]
end

puts "List created; about to iterate:"
LazyLispyEnumerable.new(list).each do |x|
  puts x
end

由于lambda函数可以延迟调用,所以我们得到一个无穷链表。

<本文完>

]]>
Implementation of A star algorithm 2014-04-09T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/04/09/a-star-algo a* 算法在人工智能领域应用比较广泛。这几天在USTC ACM上刷problem 1012,题中需要解决8-puzzle问题,有两种解法,一种是BFS,另外一种就是a*算法。之前在Java上实现过,但是当时是利用Princeton的一个类:MinPQ,最小的优先级队列。

在C++中,其实也有priority_queue。这个是一个接口封装,底层可以使用vector或者deque实现。priority_queue

priority_queue<T, Sequence, Compare>

其中T表示储存值的类型,Sequence表示底层的实现方式,Compare表示比较函数。这里需要注意的是,默认priority_queue是最大堆。

网上很多声音说这个优先级队列无法满足需求,因为无法满足动态修改优先级队列中的元素。不知道为什么priority_queue不提供迭代器,导致无法访问其内部元素。在a*算法中在某些场景中需要更新队列中的元素。

其实STL的算法中提供了三个函数,pushheap,popheap,make_heap。利用这三个函数,外加一个仿函数即可实现一个最小优先级队列。

struct Compare {
    bool operatro()(const Type &lhs, const Type &rhs)
    {
        //....
    }
};

make_heap(sequence.begin(), sequence.end(), Compare());

sequence.push_back(value);
push_heap(sequence.begin(), sequence.end(), Compare());

pop_heap(sequence.begin(), sequence.end(), Compare());
sequence.pop_back();

具体代码在:a* C++ implementation

本文完

]]>
Code Rush观后感 2014-04-03T00:00:00-07:00 aiyun-li http://lay331.github.io/2014/04/03/thoughts-of-code-rush 在等待吃饭的间隙把电影《Code Rush》看了一下,这个电影也是Jeff Atwood和阮一峰推荐的。故事的主要内容时Netscape将浏览器代码开源为Mozilla的过程。

网景公司制造了互联网公司的神话,在IPO的时候股价从20$一下子飙升至170多$。一下子让很多人成了富翁,也许这也是很多人愿意加入初创公司的原因,也为有一夜暴富的可能性。但是不知道是不是微软公司是故意的还是像Bill Gates所说的,他们不想阻止人们自由使用软件的权利。不管怎么说,微软还是利用自己在操作系统上的帝国地位完成了对网景公司的逆袭。虽然后来微软在预装软件上对浏览器采用投票的方式决定,但是可能大势已去。

网景公司知道自己在浏览器市场已经无法和微软帝国抗衡,CEO选择了将浏览器代码开源,同时支持三个平台,对于这个计划的日期定在3月31日早上10点,开放对用户的下载。在这个时间确定了以后,其实开源的代码还有很多工作需要完成,消除BUG,完成对各种平台的支持,而且要保证软件可以正常运行。

在项目截止日期还有一个月的时候,他们每个人负责自己的工作,虽然他们的工作可能不同,但是他们的在那个时候的结论是一致的,“There are doomed!”,他们死定了,因为按时发布软件似乎就是一个不可能的过程,无数的BUG,而且BUG在持续增加。有些人白天上班,有些人是夜猫子。大家都是筋疲力尽。

在这个关键时刻,还有一个程序员处于missing的状态,让大家更为着急。等到他回来的时候,项目领导者下了最后命令,必须完成,它是优秀的人,果然在一夜之间就解决了他自己的BUG。距离发布的前一天,整个团队终于将这个不可能完成的任务完成了,开始第一次在普通机器上的调试。

结果让人欢心鼓舞。但是瞬间crash了。没事,至少代码是可以运行的。

好事总是多磨,在发布的前一天,发现有一段代码时Apple,必须得到Apple的授权。他们致电给Jobs,但是好像电话没有通。最后又是那个消失的程序员完成了代码的重写。

他们觉得网景公司将浏览器代码开源是一件非常重大的事情,但是当他们联系媒体的时候,似乎不怎么受待见。媒体都认为网景已经不是以前的网景了。

在发布前1分钟,突然连不上ftp服务器,无法将代码送至指定服务器供用户下载。但是好在是虚惊一场。最后还是成功的发布了。

网景公司的开源助长了开源力量。而且也使得微软的敌人从网景公司变成了开源的群体。这是一个互联网历史上需要铭记的事件与时刻。

那些年,网景公司V5的程序员;那些年网景公司开明的决定,将浏览器源代码开源,并且成立了日后不断壮大的Mozilla基金会。

本文完

]]>