Wang Chunye's BLOG

setup Emacs IDE for C/C++

Objectives

  1. auto complete
  2. auto format on save
  3. LSP (eglot or lsp-mode)

Install clangd

cland is a language server for C/C++. install clangd lists many installation method. Here I show how to install from source codes.

download

% mkdir ~/build/llvm
% cd ~/build/llvm
% ls -l
% curl -Lo llvm-10.0.0.src.tar.xz https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0/llvm-10.0.0.src.tar.xz
% curl -Lo clang-10.0.0.src.tar.xz https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0/clang-10.0.0.src.tar.xz
% curl -Lo clang-tools-extra-10.0.0.src.tar.xz https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0/clang-tools-extra-10.0.0.src.tar.xz

extract

% cd ~/build/llvm
% tar -xvf llvm-10.0.0.src.tar.xz
% tar -xvf clang-10.0.0.src.tar.xz -C llvm-10.0.0.src/tools/
% mv ./llvm-10.0.0.src/tools/clang-10.0.0.src llvm-10.0.0.src/tools/clang
% tar -xf clang-tools-extra-10.0.0.src.tar.xz -C llvm-10.0.0.src/tools/clang/tools
% mv llvm-10.0.0.src/tools/clang/tools/clang-tools-extra-10.0.0.src llvm-10.0.0.src/tools/clang/tools/clang-tools-extra

configure, build and install

I am not sure it is a bug or not, but I have to change llvm-10.0.0.src/tools/clang/tools/CMakeLists.txt as below

--- llvm-10.0.0.src/tools/clang/tools/CMakeLists.txt.orig       2020-04-09 11:46:25.844204200 +0800
+++ llvm-10.0.0.src/tools/clang/tools/CMakeLists.txt    2020-04-09 11:46:39.615524000 +0800
@@ -36,7 +36,7 @@
 # on top of the Clang tooling platform. We keep them in a separate repository
 # to keep the primary Clang repository small and focused.
 # It also may be included by LLVM_EXTERNAL_CLANG_TOOLS_EXTRA_SOURCE_DIR.
 -add_llvm_external_project(clang-tools-extra extra)
 +add_llvm_external_project(clang-tools-extra)

 # libclang may require clang-tidy in clang-tools-extra.
 add_clang_subdirectory(libclang)
% cd ~/build/llvm/
% mkdir build_out
% cd  build_out
% cmake -DCMAKE_BUILD_TYPE=MINSIZEREL -DBUILD_SHARED_LIBS=on  ../llvm-10.0.0.src
% make && sudo make install
% which clangd # /usr/local/bin/clangd, make sure it is exits.

emacs configuration

If you are not so patient, you can install 100ms_dot_emacs, it works out of box.

eglog

(use-package eglot
  :defines (eglot-mode-map eglot-server-programs)
  :hook (((c-mode c++-mode) . eglot-ensure))
  :bind (:map eglot-mode-map
              ("C-c h" . eglot-help-at-point)
              ("C-c f r" . xref-find-references)
              ("C-c f d" . eglot-find-declaration ;; xref-find-definitions
               )
              ("C-c f D" . xref-find-definitions-other-window)
              ("C-c f t" . eglot-find-typeDefinition)
              ("C-c f i" . eglot-find-implementation)
              ("C-c =" . eglot-format-buffer)
              ("C-c c" . eglot-completion-at-point)
              ("C-c r" . eglot-rename)
              ("C-c a" . eglot-code-actions))
  :config
  (add-to-list 'eglot-server-programs '((c++-mode c-mode) "clangd")))

company

(use-package company
  :after (prog-mode)
  :diminish (company-mode . "C")
  :defines (
            company-active-map
            company-idle-delay
            company-minimum-prefix-length
            company-show-numbers
            company-tooltip-limit
            company-dabbrev-downcase)
  :hook (prog-mode . company-mode)
  :bind (:map company-active-map
              (("C-n" . company-select-next)
               ("C-p" . company-select-previous)))
  :config
  (setq company-idle-delay              nil)
  (setq company-minimum-prefix-length   2)
  (setq company-show-numbers            t)
  (setq company-tooltip-limit           20)
  (setq company-dabbrev-downcase        nil)
  :bind (:map prog-mode-map
              ("C-r" . company-complete)))

format on save

(use-package clang-format
  :defines (clang-format-fallback-style)
  :after (cc-mode)
  :config
  (set-default 'clang-format-fallback-style "Google")
  (add-hook 'c-mode-common-hook #'(lambda()
                                    (add-hook 'before-save-hook
                                              'clang-format-buffer t t))))

for Makefile project

We need to install Bear

% mkdir ~/build
% cd ~/build
% git clone --depth 1 git@github.com:rizsotto/Bear.git
% cd Bear
% cmake .
% make
% sudo make install

We create a simple hello world project as below

# Makefile
hello: hello.c
#include <stdio.h>
struct point {
   int x;
   int y;
};
int main(int char, char argv[]) {
   struct point pt;
   pt.x ; # press C-r to complete, C-r is bind to company-complete
}
% bear make # this generate compile_commands.json

for cmake project

cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=on . # this command generate compile_commands.json

common used key bindigns

keycommand
M-7compile
C-rcompany-complete
M-.xref-find-definitions
M-,xref-pop-marker-stack

setup emacs IDE for Rust

Objectives:

  1. generate a simple hello world in Rust.
  2. format on save
  3. auto completion
  4. code navigation
  5. cargo integration

use 100ms_dot_emacs

If you are not so patient, you can install 100ms_dot_emacs, it works out of box.

generate a new project

https://doc.rust-lang.org/cargo/guide/creating-a-new-project.html

% cargo new hello_world --bin
     Created binary (application) `hello_world` package
% tree .
.
└── hello_world
    ├── Cargo.toml
    └── src
        └── main.rs

2 directories, 2 files
chunywan@localhost:rust% cd hello_world/
chunywan@localhost:hello_world% cargo build
   Compiling hello_world v0.1.0 (/Users/chunywan/d/working/learn/rust/hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 1.80s
chunywan@localhost:hello_world% cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/hello_world`
Hello, world!

install emacs rust-mode

https://github.com/rust-lang/rust-mode

format on save

install rustfmt

https://github.com/rust-lang/rustfmt

rustup component add rustfmt
M-x rust-format-buffer

you might need exec-path-from-shell if you are using mac os.

https://github.com/purcell/exec-path-from-shell

M-x package-install RET exec-path-from-shell RET.
(use-package exec-path-from-shell
  :ensure t
  :config
  (when (or (memq window-system '(mac ns x))
            (memq system-type '(darwin)))
    (exec-path-from-shell-initialize)))
(use-package rust-mode
  :ensure t
  :config
  (setq rust-format-on-save t))

auto completion

install racer

https://github.com/racer-rust/racer#installation

% cargo install racer
% rustup toolchain add nightly
% cargo +nightly install racer
% rustup component add rust-src

https://github.com/racer-rust/emacs-racer

(use-package rust-mode
  :mode "\\.rs\\'"
  :ensure t
  :config
  (setq rust-format-on-save t)
  (add-hook 'rust-mode-hook #'cargo-minor-mode))
(use-package racer
  :ensure t
  :config
  :after (rust-mode)
  :config
  (add-hook 'rust-mode-hook #'racer-mode)
  (add-hook 'racer-mode-hook #'eldoc-mode)
  (add-hook 'racer-mode-hook #'company-mode)
  (define-key rust-mode-map (kbd "TAB") #'company-indent-or-complete-common)
  (setq company-tooltip-align-annotations t)
  (setq racer-loaded 1))

You can input std::io::B<TAB> , to test whether it works or not.

To test go to definition: Place your cursor over a symbol and press M-. to jump to its definition.

Press C-x 4 . to jump to its definition in another window.

Press C-x 5 . to jump to its definition in another frame.

Press M-, to jump back to the previous cursor location.

If it doesn't work, try M-x racer-debug to see what command was run and what output was returned.

In this way, we can navigate in source code.

cargo integration

(add-hook 'rust-mode-hook #'cargo-minor-mode)

cargo-minor-mode is enabled in rust mode, now we can use the following cargo commands.

C-c C-c C-a     cargo-process-add
C-c C-c C-b     cargo-process-build
C-c C-c C-c     cargo-process-repeat
C-c C-c C-d     cargo-process-doc
C-c C-c C-e     cargo-process-bench
C-c C-c C-f     cargo-process-current-test
C-c C-c TAB     cargo-process-init
C-c C-c C-k     cargo-process-check
C-c C-c C-l     cargo-process-clean
C-c C-c RET     cargo-process-fmt
C-c C-c C-n     cargo-process-new
C-c C-c C-o     cargo-process-current-file-tests
C-c C-c C-r     cargo-process-run
C-c C-c C-s     cargo-process-search
C-c C-c C-t     cargo-process-test
C-c C-c C-u     cargo-process-update
C-c C-c C-v     cargo-process-doc-open
C-c C-c C-x     cargo-process-run-example
C-c C-c C-S-a   cargo-process-audit
C-c C-c C-S-d   cargo-process-rm
C-c C-c C-S-k   cargo-process-clippy
C-c C-c C-S-o   cargo-process-outdated
C-c C-c C-S-u   cargo-process-upgrade

language server support

% rustup component add rls rust-analysis rust-src
(use-package lsp-mode
  :defines (lsp-keymap-prefix)
  :commands (lsp lsp-deferred)
  :init (setq lsp-keymap-prefix "C-l")
  :hook (((rust-mode)
          . lsp-deferred)
         (lsp-mode . lsp-enable-which-key-integration)))

use emacs to control tmux

There are two main use cases for using emacs to control tmux session.

  1. Recording your command history in a markdown file.
  2. Bash programming

How it works

tmux has many subcommands to control tmux, one of is send-keys, for example,

tmux send-keys -t ! ls

It sends ls to the last active window. refer to tmux manual for detail about -t

It is not easy to send control characters, like return, tab etc. Fortunately with latest tmux 3.0a, it supports -H command line option, e.g.

tmux send-keys -t ! -H 6c 73 0a

0a means return key so that we can execute ls in the last active window.

In elisp, we can easily convert any string into hex format as below

(defun tmux-cc--convert-keys(strings)
  (seq-map #'(lambda(c) (format "%x" c)) strings))
(tmux-cc--convert-keys "ls\n") => ("6c" "73" "a")

It becomes interesting when we invoke tmux send-keys from within a emacs session.

(setq strings "ls\n")
(apply #'call-process
   `("tmux" nil "*tmux cc*" t
     "send-keys" "-t" "op" "-H" ,@(tmux-cc--convert-keys strings)))

In this way, we can send arbitrary strings from a emacs buffer to a tmux session.

There is a complete implemenation in https://github.com/wcy123/tmux-cc

To install the package, you can put the following lines in your ~/.emacs.

(use-package tmux-cc
  :straight
  (tmux-cc :type git
           :host github
           :repo "wcy123/tmux-cc")
  :commands
  (tmux-cc-send-current-line tmux-cc-select-block tmux-cc-send-region))

And it is recommended to bind C-z in markdown-mode or shell-script-mode.

(use-package markdown-mode
  :defines (markdown-mode-map)
  :mode "\\.md\\'"
  :mode "\\.markdown\\'"
              ("C-z" . tmux-cc-send-current-line))

Or you can just install https://github.com/wcy123/100ms_dot_emacs it works out of box.

2020-04-04 start to use mdbook to write my blog

download and install mdbook

refer to mdbook@github

cargo install mdbook

create a book

mkdir ~/d/working/wcy123.github.com/
mdbook init

I modifiy book.toml as following, copy some configurations from mdBook/book-example/book.toml

[book]
authors = ["WangChunye"]
language = "cn"
multilingual = true
src = "src"
title = "Wang Chunye"


[output.html]
mathjax-support = true
google-analytics = "UA-98267158-1"

[output.html.playpen]
editable = true
line-numbers = true

[output.html.search]
limit-results = 20
use-boolean-and = true
boost-title = 2
boost-hierarchy = 2
boost-paragraph = 1
expand = true
heading-split-level = 2

We can include another file by using a preprocessor, the above is generated by

```toml
{{#include ../book.toml}}
```

refer to mdBook-specific markdown for detail

start the server

mdbook serve
open http://localhost:3000

MathJax support

block equations

\[ (a+b)^2 = a^2 + 2ab + b^2 \]

where \( (a+b)^2 = a^2 + 2ab + b^2 \) is an inline equations

refer to MathJax Support for detail

source code syntax high light

#include <stdio.h>
int main(int argc, char* argv[]) {
    printf("hello world\n");
}

#![allow(unused_variables)]
fn main() {
fn foo() -> i32 {
    1 + 1
}
}

CI integration and deploy automation

  • create a github token

    refer to creating a token. for detail.

  • enable travis ci

    refer to https://travis-ci.org/github/

    activate Travis CI and set environment variable GITHUB_TOKEN to your token.

    read secure your token

  • deploy automation

read Running mdbook in Continuous Integration and GitHub Pages Deployment

edit <PROJECT_ROOT>/.travis.yml

language: rust
sudo: false

cache:
  - cargo

rust:
  - stable

before_script:
  - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
  - (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.3" mdbook)
  - cargo install-update -a

script:
  - mdbook build . && mdbook test .


deploy:
  provider: pages
  skip-cleanup: true
  github-token: $GITHUB_TOKEN
  local-dir: book
  keep-history: false
  target_branch: master
  on:
    branch: pandoc

NOTE: we must set the local-dir to book which is the output directory of mdbook

theme

# get the default theme
mkdir -p $HOME/tmp/book
cd $HOME/tmp/book
mdbook init --theme
cd - # go back to your book directory
cp -av $HOME/tmp/book/src/theme ./ # copy default theme
curl -sLo src/theme/highlight.css  cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.1/styles/solarized-light.min.css

NOTE: it is confusing whether the theme directory is ./theme or ./src/theme, it seems ./theme is preferred.

从一个求和函数谈起"

有一个简单的程序,对一个整数数组求和,我们试着用几种不同的方式实现。

第一种方式 goto

int sum(int *begin, int *end) {
  int ret = 0;
  int *p = begin;
loop:
  if (p == end) {
    goto end;
  }
  ret = ret + *p;
  p = p + 1;
  goto loop;
end:
  return ret;
}

第二种方式 while

int sum(int *begin, int *end) {
  int ret = 0;
  int *p = begin;
  while (p != end) {
    ret = ret + *p;
    p = p + 1;
  }
  return ret;
}

第三种方式 for

int sum(int *begin, int *end) {
  int ret = 0;
  for(int *p = begin; p != end; p = p + 1){
    ret = ret + *p;
  }
  return ret;
}

第四种方式 foreach,需要 c++11 标准

struct range {
  int * begin_;
  int * end_;
  int * begin() {return begin_;}
  int * end() {return end_;}
};
int sum(int *begin, int *end) {
  int ret = 0;
  for (int x : range{begin, end}) {
    ret = ret + x;
  }
  return ret;
}

第五种方式,accumulate

int sum(int *begin, int *end) {
  return std::accumulate(begin, end, 0, std::plus<int>());
}

goto 的实现方式是最灵活的,有可能实现麻花式的流程控制。

相对 goto 来说,while 是一种固定模式的循环结构。于是while 抽象出来这种循环模式。

相对 while 来说,for 更加强调了规范的循环结构。

for-eachfor 的基础上,加入了更多的限制,我们无法看到循环的游标变量了。某种程度上,防止了内存非法访问。

accumulate 类似的,加入更多的限制。

每种方式增加了一层抽象,逐步提高了代码的可读性。

关于性能,我们可以试着用下面的命令看一下

g++ -std=c++11 -ggdb -o a.out -Os a.cpp

如果我们使用 -Os 优化开关,五种方式生成了一模一样的汇编代码。在某些情况下,c++ 增加了代码抽象级别,提高了可读性,而且不损失性能。

c++ 可否同时抛出两个异常

在 C++ 中,如果我们抛出异常后 ,在捕获异常之前,会析构掉所有还在堆栈上的对象。

#include <iostream>
using namespace std;
class Object {
  public:
   ~Object();
};
Object::~Object() { cout << "Deconstructor is invoked." << endl; }
void foo() {
    try {
        Object obj;
        throw invalid_argument("only for testing");
    }catch(const exception & e) {
        cout << "catch exception: " << e.what() << endl;
    }
}
int main(int argc, char *argv[])
{
    foo();
    return 0;
}

这个时候,程序的输出是

Deconstructor is invoked.
catch exception: only for testing

这里只有一层函数调用,如果有多层函数调用,也是类似的。

#include <iostream>
using namespace std;
class Object {
  public:
   Object(int value) : value_(value){};
   ~Object();
   int value_;
};
Object::~Object() {
  cout << "Deconstructor is invoked. value=" << value_ << endl;
}
void foo3() { throw invalid_argument("only for testing"); }
void foo2() {
    auto object = Object(2);
    foo3();
}
void foo1() {
    auto object = Object(1);
    foo2();
}
int main(int argc, char *argv[])
{
    try {
        foo1();
    }catch(const exception & e) {
        cout << "catch exception: " << e.what() << endl;
    }
    return 0;
}

程序输出结果是

Deconstructor is invoked. value=2
Deconstructor is invoked. value=1
catch exception: only for testing

可以看到,堆栈上的所有对象都被析构掉了。调用析构函数的顺序和调用构造函数的顺序相反。也就是说,堆栈上的对象按照被构造的顺序,反序析构。

什么叫做“同时抛出两个异常” ? 在上面的例子中,如果我们在析构函数中再抛出一个异常,这样在捕获异常之前,就会同时存在两个异常。

#include <iostream>
using namespace std;
class Object {
  public:
   ~Object();
};
Object::~Object() {
  cout << "Deconstructor is invoked." << endl;
  throw invalid_argument("another exception");
}
void foo() {
    try {
        Object obj;
        throw invalid_argument("only for testing");
    }catch(const exception & e) {
        cout << "catch exception: " << e.what() << endl;
    }
}
int main(int argc, char *argv[])
{
    foo();
    return 0;
}

在 C++ 中,如果像这样同时存在两个异常,那么程序会调用 std::terminate ,程序异常退出。

上面的例子中,输出结果是

Deconstructor is invoked.
libc++abi.dylib: terminating with uncaught exception of type std::invalid_argument: another exception

于是,不要在析构函数里面抛出异常

C++ 中的 converting constructor

如果构造函数可以只传递一个参数,那么这个构造函数又叫做类型转换构造函数, converting constructor 。参考 https://en.cppreference.com/w/cpp/language/converting_constructor

converting constructor 很有用,也容易被误用。

例如

struct Foo {
  Foo(int value);
};

Foo::Foo(int value) { //
  cout << "Foo(int) is called, value=" << value;
}

应用场景

主要有以下几种应用场景

构造一个对象

Foo obj(10);

这种是很普通的调用,这个时候,converting constructor 看起来和普通构造函数一样,没有什么特殊的地方。

用不同类型直接赋值

  Foo obj = 10;

这个时候会调用调用 Foo::Foo(int)

函数参数

void fun1(Foo v) {
}
int main(int argc, char *argv[])
{
  fun1(100);
  return 0;
}

虽然 fun1 接收参数的类型是 Foo ,然而我们可以传递一个 int 类型的参数给 fun1 ,这个时候,会调用 Foo::Foo(int) ,构造一个临时 Foo 对象,然后把这个临时对象传递给 fun1

函数返回值

Foo fun2() {
    return 100;
}
int main(int argc, char *argv[])
{
    fun2();
}

类似的,尽管 fun2 的返回值类型是 Foo ,但是依然可以使用 return 100; ,这个时候,使用 Foo::Foo(int) 构造一个临时对象,fun2 返回这个临时对象。

正确的使用,会提高代码的可读性

很多标准库中的类,例如 std::vectorstd::string 都有看起来不错的 converting constructor ,可以使代码的可读性很好。

string a = "hello world";
vector<int> v = {1,2,3};

converting constructor 还可以组合起来。例如

void fun3(const vector<string>& vs){
    int c = 0;
    for(auto & s: vs) {
      cout << "vs[" << c++ << "] = " << s << endl;
    }
}
int main(int argc, char *argv[])
{
    fun3({"hello", "world"});
}

这里调用的 vector 构造函数

vector::vector( std::initializer_list<T> init,
                const Allocator& alloc = Allocator() );

尤其应该注意的是,转换构造函数可以有多个参数,如果其他参数有默认值的话。

参考 https://en.cppreference.com/w/cpp/container/vector/vector 。

其中 Tstd::string ,于是又调用

basic_string( const CharT* s,
              size_type count,
              const Allocator& alloc = Allocator() );

参考 https://en.cppreference.com/w/cpp/string/basic_string/basic_string 。

类似的,也可以作为返回值。

vector<string> fun4() { return {"hello", "world"}; }

int main(int argc, char *argv[])
{
    int c = 0;
    for(auto & s: fun4()) {
      cout << "vs[" << c++ << "] = " << s << endl;
    }
}

这样的代码,看起来十分简洁。

错误的使用,代码会有隐藏的 bug

// 这段代码会有编译错误
vector<string> fun5() { return 10u; }

这里会调用 vector( size_type count ); ,为了避免这种错误,标准库里面的定义是

explicit vector( size_type count );
explicit vector( size_type count, const Allocator& alloc = Allocator() );

关键字 explicit 起到了应有的作用,尽管 vector(size_type count) 只有一个参数,因为 explicit 关键字的存在,这个构造函数不是一个转换构造函数。

在实际项目中,除了通用性非常好的标准库之外,大多数情况下,我们都不需要使用转换构造函数。转换构造函数看起来很酷,实际上有一些缺点,违反了一些原则。

  1. 显式比隐式的要好
  2. 破坏了 C++ 中的类型安全机制。
  3. 考虑到函数重载 function overloading ,很容易搞不清楚调用了哪一个版本的函数。
  4. 破坏了 POLA 原则,https://en.wikipedia.org/wiki/Principle_of_least_astonishment 。有些场景,会让用户很吃惊。

实际上,大多数的只有一个参数的构造函数都应该加上了 explicit 关键字。

"深入理解 Return Value Optimization

让我们用一个例子来看看 g++ 的 RVO ( Return Value Optimization ) 是怎么工作的。

#include <iostream>
using namespace std;
int c = 0;

class Foo {
 public:
  explicit Foo();
  Foo(const Foo& other);
  ~Foo();
  int value;
};

ostream& operator<<(ostream& out, const Foo& v) {
  out << "Foo[" << v.value << "@" << (void*)&v << "]";
  return out;
}

Foo::Foo() : value(c++) { cout << "construct: " << *this << endl; }
Foo::Foo(const Foo& other) : value(other.value) {
  cout << "copy from " << other << "to " << *this << endl;
}
Foo::~Foo() { cout << "deconstructor: " << *this << endl; }

Foo build() {
  int mark = 0;
  cout << "&mark " << (void*)&mark << endl  //
      ;
  return Foo();
}

int main(int argc, char* argv[]) {
  cout << "begin block" << endl;
  {
    int begin = 0;
    auto obj = build();
    int end = 0;
    cout << "&begin " << (void*)&begin << endl  //
         << "obj = " << obj << endl             //
         << "&end " << (void*)&end << endl      //
        ;
  }
  cout << "end block" << endl;
  return 0;
}

如果我们没有指定编译选项,g++ 默认打开了 RVO 的优化开关

% g++ -std=c++11 test_rvo.cpp
% ./a.out
begin block
&mark 0x7fff5476766c
construct: Foo[0@0x7fff54767708]
&begin 0x7fff5476770c
obj = Foo[0@0x7fff54767708]
&end 0x7fff54767704
deconstructor: Foo[0@0x7fff54767708]
end block

我们知道 x86 平台下,堆栈是向下生长的,也就是说,堆栈上的地址分配如下。

# main 函数的堆栈空间
0x7fff5476770c: &begin
0x7fff54767708: &obj
0x7fff54767704: &end
...
# build 函数的堆栈空间
0x7fff5476766c: &mark

可以看到,build() 函数在构造 Foo 对象的时候,实际上使用的是 main 函数中的堆栈地址空间。换句话说,在调用 build 之前,Foo 对象的内存就已经提前分配好了,使用的是 main 函数的堆栈地址空间,而 Foo 对象的初始化是在调用 build 函数之后执行的。

同时我们注意到,拷贝构造函数没有被调用。

我们试试关闭 RVO 。

% g++ -fno-elide-constructors -std=c++11 test_rvo.cpp
% ./a.out
begin block
&mark 0x7fff52a1366c
construct: Foo[0@0x7fff52a13668]
copy from Foo[0@0x7fff52a13668]to Foo[0@0x7fff52a13700]
deconstructor: Foo[0@0x7fff52a13668]
copy from Foo[0@0x7fff52a13700]to Foo[0@0x7fff52a13708]
deconstructor: Foo[0@0x7fff52a13700]
&begin 0x7fff52a1370c
obj = Foo[0@0x7fff52a13708]
&end 0x7fff52a136f0
deconstructor: Foo[0@0x7fff52a13708]
end block

-fno-elide-constructors 表示关闭 RVO 优化开关。

堆栈分析

# main 函数的堆栈空间
0x7fff52a1370c: &begin
0x7fff52a13708: &obj
0x7fff52a13700: &拷贝的临时对象 tmp-obj2
0x7fff52a136f0: &end
# build 函数的堆栈空间
...
0x7fff52a1366c: &mark
0x7fff52a13668: &构造临时对象 tmp-obj1

我们看到,拷贝构造函数被调用了两次。我们仔细看一下发生了什么。

  1. 在 main 函数的堆栈空间上,预留内存 0x7fff52a13700 ,准备接收返回值 tmp-obj2
  2. 在 build 函数的堆栈空间上,申请内存 0x7fff52a13668 ,并且构造了临时对象 tmp-obj1
  3. 调用拷贝构造函数,把 tmp-obj1 拷贝到 tmp-obj2 上。
  4. build 函数在返回之前,调用析构函数,析构掉 tmp-obj1
  5. 在 main 函数中,调用拷贝构造函数,把 tmp-obj2 拷贝构造到 obj 变量上。
  6. 立刻析构掉临时对象 tmp-obj2
  7. 继续在执行 main 函数之后的代码,在代码块结束的时候,调用析构函数,析构掉 obj 对象。

RVO 的优化实在是太有用了,以至于编译器默认是打开这个优化开关的。

我们可以经常使用类似下面的代码

HeavyClass foo();

// at call site
HeavyClass obj = foo();

这种代码可读性好,而且我们不用担心效率的问题, RVO 可以保证代码十分高效的运行。

在实际项目中,我会看到下面的代码

void foo(HeavyClass * ret);

// at call site
HeavyClass obj;
foo(&obj);

这种方式可读性不好。 作者本来的目的是防止多次拷贝对象,然而, 这样通常导致一次多余的函数调用。 因为一般我们在 foo 函数里面要构造一个对象,然后拷贝到 obj 。更糟糕的是, obj 对象被初始化两次,第一次初始化是在调用 foo 之前,使用默认构造函数。这个时候 obj 对象是一个无意义的对象。因为 RAII 的语义,导致这个设计是很丑陋的。

了解 ELF 文件格式

ELF 是指 Executable and Linkable Foramt ,是 LINUX 下最常用的可执行文件格式。https://en.wikipedia.org/wiki/Executable_and_Linkable_Format 有一个大略的介绍。 man elf 可以得到更详细的介绍。

本文试图只用 echo 的命令,而不使用其他任何命令,构造一个 hello world 的二进制可执行程序。这个程序的意义是

  1. 深入了解 ELF 的文件格式。
  2. 了解机器指令
  3. 了解 linux 系统调用

首先,按照文档构造 ELF 文件的 64个字节的头部。

echo -ne '\x7fELF' # EI_MAGIC0 .. EI_MAGIC3
echo -ne '\x02'    # EI_CLASS, 64-bit format
echo -ne '\x01'    # EI_DATA, small endian
echo -ne '\x01'    # EI_VERSION = 1, the original version of ELF
echo -ne '\x00'    # EI_OSABI = 0, System V
echo -ne '\x00'    # EI_ABIVERSION = 0
echo -ne '\x00\x00\x00\x00\x00\x00\x00'    # EI_PAD unused
echo -ne '\x03\x00'  # e_type = ET_DYN
echo -ne '\x3e\x00'  # e_machine = 0x3e, x86_64
echo -ne '\x01\x00\x00\x00'  # e_version = 1, the orignal version of ELF
echo -ne '\x78\x00\x00\x00\x00\x00\x00\x00' # e_entry
echo -ne '\x40\x00\x00\x00\x00\x00\x00\x00' # e_phoff
echo -ne '\x00\x00\x00\x00\x00\x00\x00\x00' # e_shoff
echo -ne '\x00\x00\x00\x00' # e_flags = 0
echo -ne '\x40\x00' # e_ehsize = 64
echo -ne '\x38\x00' # e_phentsize = 56
echo -ne '\x01\x00' # e_phnum = 1
echo -ne '\x40\x00' # e_shentsize = 64
echo -ne '\x00\x00' # e_shnum = 0
echo -ne '\x00\x00' # e_shstrn

这里为了简化,只有一个 program header ,而没有 section header 。需要注意的是 e_entry ,这个是程序入口地址,是 0x78 。后面可以看到,0x78 是第一个机器指令的偏移量。

这个 program herader table 起始于文件偏移量 0x40 处,即 e_phoff,也就是说,64 字节的 ELF header 后面紧跟着一个 program header table,而这个表格里面只有一项 program header。我们开始构造 56 字节的 program header 。

echo -ne '\x01\x00\x00\x00' # p_type = PT_LOAD
echo -ne '\x05\x00\x00\x00' # p_flag = R_E, readonly, and exectable
echo -ne '\x00\x00\x00\x00\x00\x00\x00\x00'  # p_offset = 0x78
echo -ne '\x00\x00\x00\x00\x00\x00\x00\x00'  # v_vaddr = 0x0000
echo -ne '\x00\x00\x00\x00\x00\x00\x00\x00'  # p_vaddr = 0x0000
echo -ne '\xd8\x00\x00\x00\x00\x00\x00\x00'  # p_filesz = 0x100
echo -ne '\xd8\x00\x00\x00\x00\x00\x00\x00'  # p_memsz = 0x100
echo -ne '\x00\x00\x20\x00\x00\x00\x00\x00'  # p_align = 0x200000

首先指定类型是 PT_LOAD ,这样,加载器就会把整个可执行程序映射到一个虚拟内存上。

p_flag = 0x5 指明虚拟内存的属性是只读的,是可执行的。

最后,我们写入机器指令。

echo -ne '\xba\x0d\x00\x00\x00'         # mov    $0xc,%edx
echo -ne '\x48\x8d\x35\x1e\x00\x00\x00' # lea    0x1e(%rip),%rsi
echo -ne '\x48\xc7\xc7\x01\x00\x00\x00' # mov    $0x1,%rdi
echo -ne '\x48\xc7\xc0\x01\x00\x00\x00' # mov    $0x1,%rax
echo -ne '\x0f\x05'                     # syscall
echo -ne '\x48\xc7\xc7\x4c\x00\x00\x00' # mov    $0x4c,%rdi
echo -ne '\xb8\x3c\x00\x00\x00'         # mov    $0x3c,%eax
echo -ne '\x0f\x05'                     # syscall
echo -ne 'hello world!\n\x00'           # the data

这段代码,可以参考 man syscalls, man syscall 。等效的 c 代码就是

sys_call(WRITE, 1, "hello world!\n", 14);
sys_call(EXIT, 0x4c);

为了简化,这里没有任何函数调用。生成的可执行程序如下:

% bash hello.sh | xxd
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0300 3e00 0100 0000 7800 0000 0000 0000  ..>.....x.......
00000020: 4000 0000 0000 0000 0000 0000 0000 0000  @...............
00000030: 0000 0000 4000 3800 0100 4000 0000 0000  ....@.8...@.....
00000040: 0100 0000 0500 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: b000 0000 0000 0000 b000 0000 0000 0000  ................
00000070: 0000 2000 0000 0000 ba0d 0000 0048 8d35  .. ..........H.5
00000080: 1e00 0000 48c7 c701 0000 0048 c7c0 0100  ....H......H....
00000090: 0000 0f05 48c7 c74c 0000 00b8 3c00 0000  ....H..L....<...
000000a0: 0f05 6865 6c6c 6f20 776f 726c 6421 0a00  ..hello world!..

总共 176 (0xb0) 个字节,假设这个程序叫做 a.out 。

% bash hello.sh > a.out
% chmod +x a.out
% ./a.out
hello world!
%

这里可以看到 'ba 0d' 在 0x78 的偏移量处,也就是第一条机器指令的位置,所以 e_phentry = 0x78 。

整个可执行程序的长度是 176 (0xb0) 个字节, 所以第一个 program header 中的 filesz 和 memsz 都是 0xb0。

在实际使用中,我们不使用编译器,连接器,而几乎手工构造一个可执行程序,这样做意义不大。通过这个实验,我们可以解密 linux 可执行程序的魔术,让可执行程序看起来不再那么神秘了。

vector.emplace_back 调用构造函数

vector::emplace_back 会以出乎我们意料之外的方式,调用元素的析构函数和构造函数。

原因是因为 vector 在创建的时候,会申请一个内存,当内存不够的时候,申请更多的内存,然后调用构造函数,把原有的元素复制或者移动到新的内存上,然后析构掉原有的内存。

这里有一个例子:

#include <vector>
#include <iostream>
using namespace std;

class Foo {
  public:
    explicit Foo(int i) : i_{i}{
        std::cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<<"]"//
                  << "this "  << (void*)this << " " //
                  << "i "  << i_ << " " //
                  << std::endl;
    }
#ifdef COPY_CTOR
    Foo(const Foo& other) : i_ {other.i_} {
        std::cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<<"]"//
                  << "this "  << (void*)this << " " //
                  << "other.i "  << i_ << " " //
                  << std::endl;
    }
#endif
#ifdef MOVE_CTOR
    Foo(Foo&& other) NOEXCEPT : i_ {other.i_}  {
        std::cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<<"]"//
                  << "this "  << (void*)this << " " //
                  << "other.i "  << i_ << " " //
                  << std::endl;
    }
#endif
    ~Foo() {
        std::cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<<"]"//
                  << "this "  << (void*)this << " " //
                  << "i "  << i_ << " " //
                  << std::endl;
    }
    int i_;
};

int main(int argc, char *argv[])
{
    vector<Foo> v;
    v.emplace_back(0);
    Foo& v0 = v[0];
    cout << "before push: v.capacity() = " << v.capacity() << endl;
    cout << "v.size() = " << v.size() << endl;
    v.emplace_back(1);
    cout << "after push: v.capacity() = " << v.capacity() << endl;
    cout << "&v0 is dangling pointer: " << (void*)&v0 << endl;
    return 0;
}

我们只定义拷贝构造函数的情况下。

% g++ -DNOEXCEPT=  -DCOPY_CTOR -std=c++11 vector_data_race.cpp
vector_data_race.cpp:8: [Foo::Foo(int)]this 0x7fcdeb400690 i 0 
before push: v.capacity() = 1
v.size() = 1
vector_data_race.cpp:8: [Foo::Foo(int)]this 0x7fcdeb4006a4 i 1 
vector_data_race.cpp:15: [Foo::Foo(const Foo &)]this 0x7fcdeb4006a0 other.i 0 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fcdeb400690 i 0 
after push: v.capacity() = 2
&v0 is dangling pointer: 0x7fcdeb400690
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fcdeb4006a4 i 1 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fcdeb4006a0 i 0 

在调用 v1.push 之前, capacity = 1 , size = 1 ,这个时候在追加一个元素 v2 的时候,发生了以下事情:

  1. 内存空间不足了,需要申请新的空间。
  2. 申请新的空间之后,在新的空间上构造 v1 对象。
  3. 然后调用拷贝构造函数,把 v0 拷贝到新的内存上。
  4. 然后调用析构函数,析构掉 v0 对象。

这个时候, v0 依然拿着 v[0] 的引用,是悬空引用, 我们查看 v0 的内存地址,可以看到,在这个地址上,我们已经调用过了析构函数。

如果没有定义拷贝构造函数,而是定义移动构造函数,那么 vector 会调用拷贝构造函数。

% g++ -DNOEXCEPT=  -DMOVE_CTOR -std=c++11 vector_data_race.cpp
vector_data_race.cpp:23: [Foo::Foo(Foo &&)]this 0x7ff4a7c006a0 other.i 0 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7ff4a7c00690 i 0 
after push: v.capacity() = 2
&v0 is dangling pointer: 0x7ff4a7c00690
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7ff4a7c006a4 i 1 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7ff4a7c006a0 i 0 

如果同时定义拷贝构造函数和移动构造函数的话,vector 调用的是拷贝构造函数还是移动构造函数呢?

% g++ -DNOEXCEPT= -DCOPY_CTOR -DMOVE_CTOR -std=c++11 vector_data_race.cpp
vector_data_race.cpp:15: [Foo::Foo(const Foo &)]this 0x7fb2be4006a0 other.i 0 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fb2be400690 i 0 
after push: v.capacity() = 2
&v0 is dangling pointer: 0x7fb2be400690
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fb2be4006a4 i 1 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fb2be4006a0 i 0 

这里可以看到,vector 调用的是拷贝构造函数。这里让人比较吃惊。一般来说,移动构造函数是比拷贝构造函数高效的。

参考 https://en.cppreference.com/w/cpp/container/vector/emplace_back 中的描述,可以知道,这个是因为我们没有声明 noexcept

Exceptions
If an exception is thrown, this function has no effect (strong exception guarantee). If T's move constructor is not noexcept and is not CopyInsertable into *this, vector will use the throwing move constructor. If it throws, the guarantee is waived and the effects are unspecified.

我们试试看,声明移动构造函数为 noexcept

% g++ -DNOEXCEPT=noexcept -DCOPY_CTOR -DMOVE_CTOR -std=c++11 vector_data_race.cpp

我们得到下面的结果

vector_data_race.cpp:23: [Foo::Foo(Foo &&)]this 0x7fabe5c006a0 other.i 0 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fabe5c00690 i 0 
after push: v.capacity() = 2
&v0 is dangling pointer: 0x7fabe5c00690
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fabe5c006a4 i 1 
vector_data_race.cpp:30: [Foo::~Foo()]this 0x7fabe5c006a0 i 0 

一定注意,如果想让 vector 调用元素的移动构造函数,一定要声明 noexcept

unique_ptr 的开销有多大

unique_ptr 的是零开销的,我们看看具体是指什么含义。

#include <memory>
using namespace std;

struct Foo{
    Foo();
    ~Foo();
};

void UniquePtr() {
    auto p = make_unique<Foo>();
}

void RawPtr() {
    auto p = new Foo();
    delete p;
}

我们编译一下,然后看看生产的汇编指令

+ g++ -Os -fno-exceptions -std=c++14 -c -o unique_ptr_overhead.o unique_ptr_overhead.cpp
+ objdump -D unique_ptr_overhead.o

unique_ptr_overhead.o:	file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
__Z9UniquePtrv:
       0:	55 	pushq	%rbp
       1:	48 89 e5 	movq	%rsp, %rbp
       4:	53 	pushq	%rbx
       5:	50 	pushq	%rax
       6:	bf 01 00 00 00 	movl	$1, %edi
       b:	e8 00 00 00 00 	callq	0 <__Z9UniquePtrv+0x10>
      10:	48 89 c3 	movq	%rax, %rbx
      13:	48 89 df 	movq	%rbx, %rdi
      16:	e8 00 00 00 00 	callq	0 <__Z9UniquePtrv+0x1b>
      1b:	48 89 df 	movq	%rbx, %rdi
      1e:	e8 00 00 00 00 	callq	0 <__Z9UniquePtrv+0x23>
      23:	48 89 df 	movq	%rbx, %rdi
      26:	48 83 c4 08 	addq	$8, %rsp
      2a:	5b 	popq	%rbx
      2b:	5d 	popq	%rbp
      2c:	e9 00 00 00 00 	jmp	0 <__Z6RawPtrv>

__Z6RawPtrv:
      31:	55 	pushq	%rbp
      32:	48 89 e5 	movq	%rsp, %rbp
      35:	53 	pushq	%rbx
      36:	50 	pushq	%rax
      37:	bf 01 00 00 00 	movl	$1, %edi
      3c:	e8 00 00 00 00 	callq	0 <__Z6RawPtrv+0x10>
      41:	48 89 c3 	movq	%rax, %rbx
      44:	48 89 df 	movq	%rbx, %rdi
      47:	e8 00 00 00 00 	callq	0 <__Z6RawPtrv+0x1b>
      4c:	48 89 df 	movq	%rbx, %rdi
      4f:	e8 00 00 00 00 	callq	0 <__Z6RawPtrv+0x23>
      54:	48 89 df 	movq	%rbx, %rdi
      57:	48 83 c4 08 	addq	$8, %rsp
      5b:	5b 	popq	%rbx
      5c:	5d 	popq	%rbp
      5d:	e9 00 00 00 00 	jmp	0 <__Z6RawPtrv+0x31>

我们大致比较,可以那看到 make_uniquenew/delete 编译出来了同样的指令,几乎没有区别。

同样,我们也可以看到,在空间占用上,开销也是一样的,因为 sizeof(unique_ptr<Foo>) == sizeof(Foo*)

计算一个整数有的二进制表示中多少个 1

很早以前记着有这么一个技巧,计算一个整数的二进制表示多少个 1 。

template <typename T>
int count(T a) {
  int ret = 0;
  for (T x = a; x; x &= ~(x - 1) ^ x) {
    ret++;
  }
  return ret;
}

例如输出结果

 0x0 -> 0
 0x1 -> 1
 0x2 -> 1
 0x3 -> 2
 0x4 -> 1
 0x5 -> 2
 0x6 -> 2
 0x7 -> 3
 0x8 -> 1
 0x9 -> 2
 0xa -> 2
 0xb -> 3
 0xc -> 2
 0xd -> 3
 0xe -> 3
 0xf -> 4

这个是怎么工作的呢?

如果 a 是零,程序返回零。这个很容易看得出来。 如果不是零,那么假设 x 可以表示成为 b1 b2 b3 b4 1 0 0 0 ... 结尾有 N 个 0 ,一个 1b1 ... b4 有可能是 0 或者 1

那么 x-1 可以表示成为 b1 b2 b3 b4 0 1 1 1 ...

(x-1)^x 可以表示成为 0000 11111... 因为 b1 ^ b1 = 0 无论 b1 是 0 or 1

这样的我们得到一个 mask , x&= ~mask 就可以消除掉 x 最低位的一个 1 。于是不停地重复,需要重复几次,就说明有多少个 1 。于是我们的返回值就是简单计数器,计算循环了多少次。

这个严格上来说,效率比较高。因为循环次数是 1 的个数。

这个技巧属于完全无用的技巧,如果从可读性的角度来说,下面的代码的可读性更好。

template <typename T>
int count2(T a) {
  int ret = 0;
  T pin = 1;
  for (size_t i = 0; i < sizeof(T); ++i, pin<<=1){
      if(pin&a) ret++;
  }
  return ret;
}

对于喜欢装 13 的,抗击打能力比较强的,不怕挨揍的,的那些人,可以考虑在生产代码里面采用第一种姿势。后果自负。

用c/c++ 编写一个 list 操作程序

介绍

本文试图通过迭代编程,一点一点改进一个关于 list 操作的程序。

基本结构

struct List {
    int value;
    struct List * next;
};

这是一个单链表结构。

#include <iostream>
using namespace std;

struct List {
    int value;
    struct List * next;
};

List * MakeList (int value, List * next) {
    return new List{value, next};
}

ostream& operator<<(ostream& out, const List* head) {
    out << "(";
    for(auto i = head; i ; i = i->next){
        out << " " << i->value;
    }
    out << ")";
    return out;
}

int main(int argc, char *argv[])
{
    List * lst = MakeList(1, MakeList(2, MakeList(3, nullptr)));
    cout << lst << endl;
    return 0;
}

输出结果

( 1 2 3)

使用构造函数

上面的程序没有使用构造函数,使用构造函数,看起来更加清晰。

#include <iostream>
using namespace std;

class List {
   public:
    List(): head_(nullptr) {};
    void push_front(int value) {
        head_ = new Cons(value, head_);
    }
    bool empty() {
        return head_ == nullptr;
    }
   private:
    struct Cons{
        Cons(int value, Cons* next) : value_(value), next_(next) {}
        int value_;
        struct Cons* next_;
    };
    Cons * head_;
    friend ostream& operator<<(ostream& out, const List& lst);
};

ostream& operator<<(ostream& out, const List& lst) {
    out << "(";
    for (auto i = lst.head_; i; i = i->next_) {
        out << " " << i->value_;
    }
    out << ")";
    return out;
}

int main(int argc, char* argv[]) {
    List lst;
    lst.push_front(1);
    lst.push_front(2);
    lst.push_front(3);
    cout << lst << endl;
    return 0;
}

输出结果

( 3 2 1)

Cons 名字来源于 Lisp 系列语言。

防止内存泄露

上面的程序明显有内存泄露,我们使用智能指针,来防止内存泄露。

#include <iostream>
#include <memory>
using namespace std;

class List {
   public:
    List() : head_(nullptr){};
    void push_front(int value) {
        head_ = make_unique<Cons>(value, std::move(head_));
    }
    bool empty() { return head_ == nullptr; }

   private:
    struct Cons {
        Cons(int value, unique_ptr<Cons>&& next)
            : value_(value), next_(std::move(next)) {}
        int value_;
        unique_ptr<Cons> next_;
    };
    unique_ptr<Cons> head_;
    friend ostream& operator<<(ostream& out, const List& lst);
};

ostream& operator<<(ostream& out, const List& lst) {
    out << "(";
    for (auto i = lst.head_.get(); i; i = i->next_.get()) {
        out << " " << i->value_;
    }
    out << ")";
    return out;
}

int main(int argc, char* argv[]) {
    List lst;
    lst.push_front(1);
    lst.push_front(2);
    lst.push_front(3);
    cout << lst << endl;
    return 0;
}

输出结果

( 3 2 1)

很遗憾, make_unique 是 c++14 的内容,如果必须使用 c++11 ,那么就需要我们自己实现一下这个函数。

#if __cplusplus <= 201103L
namespace std {
   template<typename T, typename ...Args>
   unique_ptr<T> make_unique(Args &&...args) {
       return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
   }
}

使用了 unique_ptr ,我们失去了一个功能,两个 list 不能共享同一段尾巴。也许这是一个好的交换,当两个 list 共享同一段尾巴的时候,需要使用 shared_ptr ,而 unique_ptr 的开销和裸指针一样,效率很高。在大多数情况下,我们不需要这种共享数据。

从其他 container 构造一个 list

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class List {
   public:
    List() : head_(nullptr){};

    template <typename Iter>
    List(Iter begin, Iter end) : head_() {
        for (auto it = begin; it != end; ++it) {
            push_front(*it);
        }
    }

    List(const std::initializer_list<int>& c) : List(c.begin(), c.end()) {}

    template <typename Container>
    List(const Container& c) : List(c.begin(), c.end()) {}

    void push_front(int value) {
        head_ = make_unique<Cons>(value, std::move(head_));
    }
    bool empty() { return head_ == nullptr; }

   private:
    struct Cons {
        Cons(int value, unique_ptr<Cons>&& next)
            : value_(value), next_(std::move(next)) {}
        int value_;
        unique_ptr<Cons> next_;
    };
    unique_ptr<Cons> head_;
    friend ostream& operator<<(ostream& out, const List& lst);
};

ostream& operator<<(ostream& out, const List& lst) {
    out << "(";
    for (auto i = lst.head_.get(); i; i = i->next_.get()) {
        out << " " << i->value_;
    }
    out << ")";
    return out;
}

int main(int argc, char* argv[]) {
    auto lst1 = List{1, 2, 3};
    cout << lst1 << endl;

    vector<int> v1 = {4, 5, 6};
    auto lst2 = List(v1);
    cout << lst2 << endl;
    return 0;
}

输出结果

( 3 2 1)
( 6 5 4)

这里用到了 c++11 的 delegating constructors, 也就是一个构造函数,调用另一个构造函数。

高效的 append 操作

上面的例子中,我们看到列表顺序是反的。我们要顺序的追加操作,而不是从 head 插入。

这个是写本文的出发点。前面都是铺垫。本文参考在 linux 的内核代码代码中的方法。首先我们来一个简单实现。

    void push_back(int value) {
        if (!head_) {
            head_ = make_unique<Cons>(value, nullptr);
        } else {
            Cons* p = nullptr;
            for (p = head_.get(); p->next_; p = p->next_.get()) {
            }
            p->next_ = make_unique<Cons>(value, nullptr);
        }
    }

这个方法有两个问题,一个是多了一个 if 判断语句,一个是多了 for 循环语句。好的代码风格是尽量没有if for 这些语句。性能也有一点问题,每插入一个元素,都要遍历一次列表。

linux kernel 的源代码里面用间接指针,struct Cons ** tail_ ,很巧妙的实现了这个功能。我们先简单理解没有智能指针的版本。

当列表为空的时候

tail_ == &head_;
head_ == NULL;

当有一个元素的时候。

Cons e1 = {1, nullptr};
head_ == &e1;
tail_ == &head_->next;

当有两个元素的时候。

Cons e2 = {2, nullptr};
Cons e1 = {1, &e2};
head_ == &e1;
tail_ == &head_->next->next;

依次类推。增加一个间接指针之后,当我们遍历列表的时候,我们不但知道列表元素,而且还知道我们从哪里过来的。

  1. tail_ 永远不会是指向一个无效地址。 assert(*tail_ != nullptr);
  2. 当列表为空的时候,*tail_ == &head ,即指向 head_ 的地址。
  3. 当列表不为空的时候, *tail_ == &last.next ,即指向最后一个元素的 next 的地址。

我们要追加一个元素的时候。

(*tail_) = &new_entry;
tail_ = &new_enty.next_;

就可以了。为什么呢?

  1. 如果列表为空,(*tail_)=&new_entry 导致 head_ = &new_entry
  2. 如果列表不为空,(*tail_) = &new_entry 导致 last.next_ = &new_entry

然后,tail_ = &new_entry.next_ 保证 tail_ 一直指向列表最后一个元素的 next 的地址。

下面是带有智能指针的代码。

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class List {
   public:
    List() : head_(nullptr), tail_(&head_){};

    template <typename Iter>
    List(Iter begin, Iter end) : head_(), tail_(&head_) {
        for (auto it = begin; it != end; ++it) {
            push_back(*it);
        }
    }

    List(const std::initializer_list<int>& c) : List(c.begin(), c.end()) {}

    template <typename Container>
    List(const Container& c) : List(c.begin(), c.end()) {}

    void push_front(int value) {
        head_ = make_unique<Cons>(value, std::move(head_));
    }

    void push_back(int value) {
        (*tail_) = make_unique<Cons>(value, nullptr);
        tail_ = &(*tail_)->next_;
    }

    void remove(int value) {
        (*tail_) = make_unique<Cons>(value, nullptr);
        tail_ = &(*tail_)->next_;
    }

    bool empty() { return head_ == nullptr; }

   private:
    struct Cons {
        Cons(int value, unique_ptr<Cons>&& next)
            : value_(value), next_(std::move(next)) {}
        int value_;
        unique_ptr<Cons> next_;
    };
    unique_ptr<Cons> head_;
    unique_ptr<Cons> * tail_;
    friend ostream& operator<<(ostream& out, const List& lst);
};

ostream& operator<<(ostream& out, const List& lst) {
    out << "(";
    for (auto i = lst.head_.get(); i; i = i->next_.get()) {
        out << " " << i->value_;
    }
    out << ")";
    return out;
}

int main(int argc, char* argv[]) {
    auto lst1 = List{1, 2, 3};
    cout << lst1 << endl;

    vector<int> v1 = {4, 5, 6};
    auto lst2 = List(v1);
    cout << lst2 << endl;
    return 0;
}

输出结果

( 1 2 3)
( 4 5 6)

其中关键代码是

    void push_back(int value) {
        (*tail_) = make_unique<Cons>(value, nullptr);
        tail_ = &(*tail_)->next_;
    }

删除一个元素

先来一个普通版本。

    void remove(int value) {
        Cons * p = nullptr;
        for (auto i = head_.get(); i; p = i, i = i->next_.get()) {
                if (i->value_ == value) {
                    p->next_ = std::move(i->next_);
                    break;
                }
        }
    }

这个版本是有 bug 的,如果删除的是第一个元素,就挂了,因为 p 是 nullptr.

    void remove(int value) {
        Cons * p = nullptr;
        for (auto i = head_.get(); i; p = i, i = i->next_.get()) {
            if (i->value_ == value) {
                if (p != nullptr) {
                    p->next_ = std::move(i->next_);
                } else {
                    head_ = std::move(head_->next_);
                }
                break;
            }
        }
    }

这个版本工作,但是多了一个 if 判断,让代码看起来不是那么的帅气。利用间接指针,我们可以帅气的解决这个问题。

    void remove(int value) {
        for (auto i = &head_; *i; i = &((*i)->next_)) {
            if ((*i)->value_ == value) {
                (*i) = std::move((*i)->next_);
                break;
            }
        }
    }

这个代码为什么能工作? 和 tail_ 类似,在遍历的过程中 i 是一个间接指针。

  1. i 是指向遍历的对象指针的指针。
  2. *i 永远不会为 nullptr
  3. 当列表为空的时候, *i 指向头指针的地址。 assert(i == &head) , assert(*i == nullptr) assert(head == nullptr
  4. 当列表不为空的时候,*i 指向前一个元素的 next 的地址。 assert(i == &previous_element.next) ,也就是说, *i 是当前元素的地址, (*i)->value 是当前元素的值。
  5. 移动到下一个元素的时候,i = &((*i)->next_ ,保证 i 一直指向前一个元素的 next 地址。
  6. 当列表不空的时候,*i = (*i)->next_ ,就是说,修改前一个元素的 next 值,让他指向当前元素的下一个元素,(*i)->next ,这样就把当前元素给跳过去了。完成了删除的操作。
  7. 当列表为空的时候,循环立即结束。
#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class List {
   public:
    List() : head_(nullptr), tail_(&head_){};

    template <typename Iter>
    List(Iter begin, Iter end) : head_(), tail_(&head_) {
        for (auto it = begin; it != end; ++it) {
            push_back(*it);
        }
    }

    List(const std::initializer_list<int>& c) : List(c.begin(), c.end()) {}

    template <typename Container>
    List(const Container& c) : List(c.begin(), c.end()) {}

    void push_front(int value) {
        head_ = make_unique<Cons>(value, std::move(head_));
    }

    void push_back(int value) {
        (*tail_) = make_unique<Cons>(value, nullptr);
        tail_ = &(*tail_)->next_;
    }

    void remove(int value) {
        for (auto i = &head_; *i; i = &((*i)->next_)) {
            if ((*i)->value_ == value) {
                (*i) = std::move((*i)->next_);
                break;
            }
        }
    }

    bool empty() { return head_ == nullptr; }

   private:
    struct Cons {
        Cons(int value, unique_ptr<Cons>&& next)
            : value_(value), next_(std::move(next)) {}
        int value_;
        unique_ptr<Cons> next_;
    };
    unique_ptr<Cons> head_;
    unique_ptr<Cons> * tail_;
    friend ostream& operator<<(ostream& out, const List& lst);
};

ostream& operator<<(ostream& out, const List& lst) {
    out << "(";
    for (auto i = lst.head_.get(); i; i = i->next_.get()) {
        out << " " << i->value_;
    }
    out << ")";
    return out;
}

int main(int argc, char* argv[]) {
    auto lst1 = List{1, 2, 3};
    lst1.remove(1);
    cout << lst1 << endl;

    auto lst2 = List{1, 2, 3};
    lst2.remove(3);
    cout << lst2 << endl;

    return 0;
}

输出结果

( 2 3)
( 1 2)

TODO

如果 list 十分长,那么在调用析构函数的时候,有可能导致堆栈空间不够,爆栈了。

c++ 中的高维数组(2)

数值计算中,常常需要操作高维数组,C++ 很容易写出来占用内存多,而且速度慢的程序。

#include <iostream>
using namespace std;

class Video {
  public:
   Video(size_t num_of_channels, size_t width, size_t height,
         size_t num_of_colors)
       : num_of_channels_(num_of_channels),
         width_(width),
         height_(height),
         num_of_colors_(num_of_colors) {
       buf_ = new char***[num_of_channels];
       for (size_t c = 0; c < num_of_channels; ++c) {
           buf_[c] = new char**[width];
           for (size_t w = 0; w < width; ++w) {
               buf_[c][w] = new char*[height];
               for (size_t h = 0; h < height; ++h) {
                   buf_[c][w][h] = new char[num_of_colors];
                   for (size_t r = 0; r < num_of_colors; ++r) {
                       buf_[c][w][h][r] = 0.0;
                   }
               }
           }
       }
    }
    ~Video() {
        for (size_t c = 0; c < num_of_channels_; ++c) {
            for (size_t w = 0; w < width_; ++w) {
                for (size_t h = 0; h < height_; ++h) {
                    delete[] buf_[c][w][h];
                }
                delete[] buf_[c][w];
            }
            delete[] buf_[c];
        }
        delete[] buf_;
    }

   private:
    const size_t num_of_channels_;
    const size_t width_;
    const size_t height_;
    const size_t num_of_colors_;
    char **** buf_;
};


int main(int argc, char *argv[])
{
    Video v(10,1920,1080,3);
    return 0;
}

我碰到过一些真实的,代码量很大的项目,至少三次看到上面这种模式。这种方法占用内存多,速度慢。

可以用一维数组模拟高维数组。

#include <iostream>
using namespace std;

class Video {
  public:
   Video(size_t num_of_channels, size_t width, size_t height,
         size_t num_of_colors)
       : num_of_channels_(num_of_channels),
         width_(width),
         height_(height),
         num_of_colors_(num_of_colors) {
       buf_ = new char[num_of_channels * width * height * num_of_colors];
       for (size_t c = 0; c < num_of_channels; ++c) {
           for (size_t w = 0; w < width; ++w) {
               for (size_t h = 0; h < height; ++h) {
                   for (size_t r = 0; r < num_of_colors; ++r) {
                       buf_[c*width*height*num_of_colors + w*height*num_of_colors + h*num_of_colors + r] = 0.0;
                   }
               }
           }
       }
    }
    ~Video() {
        delete[] buf_;
    }

   private:
    const size_t num_of_channels_;
    const size_t width_;
    const size_t height_;
    const size_t num_of_colors_;
    char * buf_;
};


int main(int argc, char *argv[])
{
    Video v(10,1920,1080,3);
    return 0;
}

我们看看速度

% c++ -O3 -std=c++11 high_dim_array_1.cpp
% ./a.out

real	0m2.873s
user	0m2.637s
sys	0m0.226s
+ c++ -O3 -std=c++11 high_dim_array_2.cpp
+ ./a.out

real	0m0.004s
user	0m0.001s
sys	0m0.001s

和上一篇文章里面不同,经过优化开关的代码,二者差距更大。

我们分析一下内存使用。对于数据来说,二者是一样的。但是第一种方案,额外存储了很多内存指针。

  1. new char***[num_of_channels]; 需要 10 个指针。
  2. 同样道理,宽度层,需要 10 * 1920 个指针
  3. 高度层,需要 10*1920*1080 个指针
  4. 最后一层,没有额外的指针空间,因为这些就是原始数据了。

可以看到,每个指针 8 个字节(64位机器),我们额外需要大约 160M+ 的内存,存储间接指针。

buf_[c][w][h][r] = 0.0;
buf_[c*width*height*num_of_colors + w*height*num_of_colors + h*num_of_colors + r] = 0.0;

这两个相比,前者看起来更加干净。但是,我们很容易做做操作符重载

char& operator()(size_t c, size_t w, size_t h, size_t r){
   return buf_[c*width*height*num_of_colors + w*height*num_of_colors + h*num_of_colors + r];
}

这样我们就可以使用下面的语法。

 (*this)(c,w,h,r) = 0.0;

这里淡淡的吐槽一下 c++ ,(*this)[c,w,h,r] = 0.0; 也许更好,可惜 c++ 的对方括号的操作符重载,只允许一个参数。

如果非要用第一种语法形式 (*this)[c][w][h][r] 的话,(*this)[c] 返回一个对象,依次产生三个临时对象。

我们比较一下访问速度

#include <time.h>

#include <iostream>
using namespace std;

class Video {
  public:
   Video(size_t num_of_channels, size_t width, size_t height,
         size_t num_of_colors)
       : num_of_channels_(num_of_channels),
         width_(width),
         height_(height),
         num_of_colors_(num_of_colors) {
       buf_ = new char***[num_of_channels];
       for (size_t c = 0; c < num_of_channels; ++c) {
           buf_[c] = new char**[width];
           for (size_t w = 0; w < width; ++w) {
               buf_[c][w] = new char*[height];
               for (size_t h = 0; h < height; ++h) {
                   buf_[c][w][h] = new char[num_of_colors];
                   for (size_t r = 0; r < num_of_colors; ++r) {
                       buf_[c][w][h][r] = r;
                   }
               }
           }
       }
    }
    ~Video() {
        for (size_t c = 0; c < num_of_channels_; ++c) {
            for (size_t w = 0; w < width_; ++w) {
                for (size_t h = 0; h < height_; ++h) {
                    delete[] buf_[c][w][h];
                }
                delete[] buf_[c][w];
            }
            delete[] buf_[c];
        }
        delete[] buf_;
    }

    float SumOfFirstColumn();
    float SumOfFirstRows();
   private:
    const size_t num_of_channels_;
    const size_t width_;
    const size_t height_;
    const size_t num_of_colors_;
    char **** buf_;
};

float Video::SumOfFirstColumn() {
    float ret = 0.0f;
    for (size_t c = 0; c < num_of_channels_; ++c) {
        for (size_t w = 0; w < 1; ++w) {
            for (size_t h = 0; h < height_; ++h) {
                for (size_t r = 0; r < num_of_colors_; ++r) {
                    ret += buf_[c][w][h][r];
                }
            }
        }
    }
    return ret;
}
float Video::SumOfFirstRows() {
    float ret = 0.0f;
    for (size_t c = 0; c < num_of_channels_; ++c) {
        for (size_t w = 0; w < width_; ++w) {
            for (size_t h = 0; h < 1; ++h) {
                for (size_t r = 0; r < num_of_colors_; ++r) {
                    ret += buf_[c][w][h][r];
                }
            }
        }
    }
    return ret;
}

int time_diff(const struct timespec& ts_end, const struct timespec& ts_start) {
    return (ts_start.tv_sec - ts_end.tv_sec) * 1000 -
           (ts_start.tv_nsec - ts_end.tv_nsec) / 1000;
}
int main(int argc, char *argv[])
{
    Video v(10,1920,1080,3);
    struct timespec ts_start;
    struct timespec ts_end;

    clock_gettime(CLOCK_MONOTONIC, &ts_start);
    float r1 = v.SumOfFirstColumn();
    clock_gettime(CLOCK_MONOTONIC, &ts_end);
    cout << __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__ << "] "
         << "r1 " << r1 << " "
         << " it takes " << time_diff(ts_end, ts_start) << " ms"
         << endl;

    clock_gettime(CLOCK_MONOTONIC, &ts_start);
    float r2 = v.SumOfFirstRows();
    clock_gettime(CLOCK_MONOTONIC, &ts_end);
    cout << __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__ << "] "
         << "r2 " << r2 << " "
         << " it takes " << time_diff(ts_end, ts_start) << " ms"
         << endl;

    return 0;
}
#include <time.h>

#include <iostream>
using namespace std;

class Video {
  public:
   Video(size_t num_of_channels, size_t width, size_t height,
         size_t num_of_colors)
       : num_of_channels_(num_of_channels),
         width_(width),
         height_(height),
         num_of_colors_(num_of_colors) {
       buf_ = new char[num_of_channels * width * height * num_of_colors];
       for (size_t c = 0; c < num_of_channels; ++c) {
           for (size_t w = 0; w < width; ++w) {
               for (size_t h = 0; h < height; ++h) {
                   for (size_t r = 0; r < num_of_colors; ++r) {
                       buf_[c*width*height*num_of_colors + w*height*num_of_colors + h*num_of_colors + r] = r;
                   }
               }
           }
       }
    }
    ~Video() {
        delete[] buf_;
    }
    float SumOfFirstColumn();
    float SumOfFirstRow();
   private:
    const size_t num_of_channels_;
    const size_t width_;
    const size_t height_;
    const size_t num_of_colors_;
    char * buf_;
};

float Video::SumOfFirstColumn() {
    float ret = 0.0f;
    for (size_t c = 0; c < num_of_channels_; ++c) {
        for (size_t w = 0; w < 1; ++w) {
            for (size_t h = 0; h < height_; ++h) {
                for (size_t r = 0; r < num_of_colors_; ++r) {
                    ret += buf_[c * width_ * height_ * num_of_colors_ + w * height_ * num_of_colors_ + h * num_of_colors_ + r];
                }
            }
        }
    }
    return ret;
}
float Video::SumOfFirstRow() {
    float ret = 0.0f;
    for (size_t c = 0; c < num_of_channels_; ++c) {
        for (size_t w = 0; w < width_; ++w) {
            for (size_t h = 0; h < 1; ++h) {
                for (size_t r = 0; r < num_of_colors_; ++r) {
                    ret += buf_[c * width_ * height_ * num_of_colors_ + w * height_ * num_of_colors_ + h * num_of_colors_ + r];
                }
            }
        }
    }
    return ret;
}

int time_diff(const struct timespec& ts_end, const struct timespec& ts_start) {
    return (ts_start.tv_sec - ts_end.tv_sec) * 1000 -
           (ts_start.tv_nsec - ts_end.tv_nsec) / 1000;
}
int main(int argc, char *argv[])
{
    Video v(10,1920,1080,3);
    struct timespec ts_start;
    struct timespec ts_end;

    clock_gettime(CLOCK_MONOTONIC, &ts_start);
    float r1 = v.SumOfFirstColumn();
    clock_gettime(CLOCK_MONOTONIC, &ts_end);
    cout << __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__ << "] "
         << "r1 " << r1 << " "
         << " it takes " << time_diff(ts_end, ts_start) << " ms"
         << endl;

    clock_gettime(CLOCK_MONOTONIC, &ts_start);
    float r2 = v.SumOfFirstRow();
    clock_gettime(CLOCK_MONOTONIC, &ts_end);
    cout << __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__ << "] "
         << "r2 " << r2 << " "
         << " it takes " << time_diff(ts_end, ts_start) << " ms"
         << endl;
return 0;
}
% c++ -O3 -std=c++11 high_dim_array_1.cpp
% ./a.out

real    0m2.873s
user    0m2.637s
sys 0m0.226s
% c++ -O3 -std=c++11 high_dim_array_2.cpp
% ./a.out

real    0m0.004s
user    0m0.001s
sys 0m0.001s
% c++ -O3 -std=c++11 high_dim_array_3.cpp
% ./a.out
high_dim_array_3.cpp:91: [main] r1 32400  it takes 40 ms
high_dim_array_3.cpp:99: [main] r2 57600  it takes 868 ms
% c++ -O3 -std=c++11 high_dim_array_4.cpp
% ./a.out
high_dim_array_4.cpp:78: [main] r1 32400  it takes 34 ms
high_dim_array_4.cpp:86: [main] r2 57600  it takes 419 ms

SumOfFirstColumn 是连续访问内存,因为内存中,是一列一列像素连续存储的。

SumOfFirstRow 是非连续访问内存。

单独访问一个像素点的时候

  • 方法1 buf_[c][w][h][r], 需要访问四次内存,才能把内存中的像素数据搬运到寄存器中运算。
  • 方法2 buf_[c * width_ * height_ * num_of_colors_ + w * height_ * num_of_colors_ + h * num_of_colors_ + r];,需要做 6 次乘法,3 次加法,一次内存访问,才能完成数据搬移。

从结果上看,数学运算的速度要快于内存访问。当连续内存访问的时候,内存 cache 的命中率比较高,数学运算的速度并不一定快于内存访问。当非连续内存访问的时候,内存访问的 cache 命中率较低,二者的差距就更加明显了。

c++ 中的高维数组(1)

数值计算中,常常需要操作高维数组,C++ 很容易写出来占用内存多,而且速度慢的程序。

#include <iostream>
using namespace std;

class Video {
  public:
   Video(size_t num_of_channels, size_t width, size_t height,
         size_t num_of_colors)
       : num_of_channels_(num_of_channels),
         width_(width),
         height_(height),
         num_of_colors_(num_of_colors) {
       buf_ = new char***[num_of_channels];
       for (size_t c = 0; c < num_of_channels; ++c) {
           buf_[c] = new char**[width];
           for (size_t w = 0; w < width; ++w) {
               buf_[c][w] = new char*[height];
               for (size_t h = 0; h < height; ++h) {
                   buf_[c][w][h] = new char[num_of_colors];
                   for (size_t r = 0; r < num_of_colors; ++r) {
                       buf_[c][w][h][r] = 0.0;
                   }
               }
           }
       }
    }
    ~Video() {
        for (size_t c = 0; c < num_of_channels_; ++c) {
            for (size_t w = 0; w < width_; ++w) {
                for (size_t h = 0; h < height_; ++h) {
                    delete[] buf_[c][w][h];
                }
                delete[] buf_[c][w];
            }
            delete[] buf_[c];
        }
        delete[] buf_;
    }

   private:
    const size_t num_of_channels_;
    const size_t width_;
    const size_t height_;
    const size_t num_of_colors_;
    char **** buf_;
};


int main(int argc, char *argv[])
{
    Video v(10,1920,1080,3);
    return 0;
}

我碰到过一些真实的,代码量很大的项目,至少三次看到上面这种模式。这种方法占用内存多,速度慢。

可以用一维数组模拟高维数组。

#include <iostream>
using namespace std;

class Video {
  public:
   Video(size_t num_of_channels, size_t width, size_t height,
         size_t num_of_colors)
       : num_of_channels_(num_of_channels),
         width_(width),
         height_(height),
         num_of_colors_(num_of_colors) {
       buf_ = new char[num_of_channels * width * height * num_of_colors];
       for (size_t c = 0; c < num_of_channels; ++c) {
           for (size_t w = 0; w < width; ++w) {
               for (size_t h = 0; h < height; ++h) {
                   for (size_t r = 0; r < num_of_colors; ++r) {
                       buf_[c*width*height*num_of_colors + w*height*num_of_colors + h*num_of_colors + r] = 0.0;
                   }
               }
           }
       }
    }
    ~Video() {
        delete[] buf_;
    }

   private:
    const size_t num_of_channels_;
    const size_t width_;
    const size_t height_;
    const size_t num_of_colors_;
    char * buf_;
};


int main(int argc, char *argv[])
{
    Video v(10,1920,1080,3);
    return 0;
}

我们看看速度

% c++ -std=c++11 high_dim_array_1.cpp
% ./a.out

real	0m3.046s
user	0m2.801s
sys	0m0.235s
% c++ -std=c++11 high_dim_array_2.cpp
% ./a.out

real	0m0.244s
user	0m0.206s
sys	0m0.028s

后者快了 14 倍。

我们分析一下内存使用。对于数据来说,二者是一样的。但是第一种方案,额外存储了很多内存指针。

  1. new char***[num_of_channels]; 需要 10 个指针。
  2. 同样道理,宽度层,需要 10 * 1920 个指针
  3. 高度层,需要 10*1920*1080 个指针
  4. 最后一层,没有额外的指针空间,因为这些就是原始数据了。

可以看到,每个指针 8 个字节(64位机器),我们额外需要大约 160M+ 的内存,存储间接指针。

buf_[c][w][h][r] = 0.0;
buf_[c*width*height*num_of_colors + w*height*num_of_colors + h*num_of_colors + r] = 0.0;

这两个相比,前者看起来更加干净。但是,我们很容易做做操作符重载

char& operator()(size_t c, size_t w, size_t h, size_t r){
   return buf_[c*width*height*num_of_colors + w*height*num_of_colors + h*num_of_colors + r];
}

这样我们就可以使用下面的语法。

 (*this)(c,w,h,r) = 0.0;

这里淡淡的吐槽一下 c++ ,(*this)[c,w,h,r] = 0.0; 也许更好,可惜 c++ 的对方括号的操作符重载,只允许一个参数。

如果非要用第一种语法形式 (*this)[c][w][h][r] 的话,(*this)[c] 返回一个对象,依次产生三个临时对象。

Walter E. Brown 讲解 c++ 中的 metaprograming

今天看了 Walter E. Brown 的youtube 视频,介绍 c++ 中的 metaprogramming 受益非浅。 这里记录一下学到的内容。

我已经熟悉如何使用 enable_if , is_sameconditional 等等。但是从来没有想过的如何自己动手实现这些功能,觉得实现这些功能,一定非常非常难。 Brown 的视频启发我,鼓励我,去实现这些功能,没有想象中的那么难,只是因为我们不熟悉 (unfimiliar)模板编程。

true_typefalse_type

这两个定义十分简单。很多简单的东西,看上去显而易见,第一感觉是这个东西没啥用,后来慢慢体会到,这些东西十分有用。就像阿拉伯数字 0

struct true_type {
    static constexpr bool value = true;
};
struct false_type {
    static constexpr bool value = false;
};

is_same 判断两个类是否相等

首先定义主干模板 (primary template)

template<class T, class U>
struct is_same : public false_type {
};

定义一个模板,is_same<T,U> ,默认情况下,这两个类不相同。

然后部分特例化 (partial specialization)

template<class T>
struct is_same<T,T> : public true_type {
};

完整的例子如下 。

// is_same_0.cpp
#include <iostream>
using std::cout;
using std::endl;


struct true_type {
    static constexpr bool value = true;
};
struct false_type {
    static constexpr bool value = false;
};

// primary defination.
template<class T, class U>
struct is_same : public false_type {
};

// partial specialization.
template<class T>
struct is_same<T,T> : public true_type {
};


int main(int argc, char *argv[])
{
    cout << "is_same<int,int> = " << is_same<int,int>::value << endl;
    cout << "is_same<int,float> = " << is_same<int,float>::value << endl;
    cout << "is_same<int,const int> = " << is_same<int,const int>::value << endl;
    return 0;
}

输出

is_same<int,int> = 1
is_same<int,float> = 0
is_same<int,const int> = 0}

这个例子中,我们看到模板编程的一个编程模式

  1. 定义 primary template 。
  2. 然后 partial specialization 。

很多时候,我们都是遵循这个模式。 primary template 类似定义接口,表明我们的模板看起来像个什么样子。 有的时候,顺便定义默认实现。 第二步,我们部分特例化,定义和默认定义不一样的案例。

is_void 查看一个类型是否是 void

is_void<T>::value 是 true ,如果 Tvoidvoid constvoid const volatile 或者是 void const volatile

首先定义 primary template .

template<class T>
struct is_void : public false_type {
};

默认 T 不是 void

然后,我们做 partial specialization 。

// partial specialization.
template<>
struct is_void<void> : public true_type {
};
template<>
struct is_void<void const> : public true_type {
};
template<>
struct is_void<void const volatile> : public true_type {
};
template<>
struct is_void<void volatile> : public true_type {
};

这个看起来有点傻,但是可读性很好,几乎就是把我们的需求重新用 c++ 语言描述一遍。后面会有一个改进版本。

完整代码

// is_void_0.cpp
#include <iostream>
using std::cout;
using std::endl;


struct true_type {
    static constexpr bool value = true;
};
struct false_type {
    static constexpr bool value = false;
};

// primary defination.
template<class T>
struct is_void : public false_type {
};

// partial specialization.
template<>
struct is_void<void> : public true_type {
};
template<>
struct is_void<void const> : public true_type {
};
template<>
struct is_void<void const volatile> : public true_type {
};
template<>
struct is_void<void volatile> : public true_type {
};


int main(int argc, char *argv[])
{
    cout << "is_void<int> = " << is_void<int>::value << endl;
    cout << "is_void<void> = " << is_void<void>::value << endl;
    cout << "is_void<void const> = " << is_void<void const>::value << endl;
    cout << "is_void<void volatile> = " << is_void<void volatile>::value << endl;
    cout << "is_void<void const volatile> = " << is_void<void const volatile>::value << endl;
    return 0;
}

程序输出

is_void<int> = 0
is_void<void> = 1
is_void<void const> = 1
is_void<void volatile> = 1
is_void<void const volatile> = 1

remove_cv 删除一个类的 const volatile 修饰

remove_cv<void>, remove_cv<void const> ,remove_cv<void volatile>remove_cv<void const volatile> 都是 void

定义 primary template

// partial specialization
template<class T>
struct remove_cv<T const> {
    using type = T;
};

定义 partial specialization

// partial specialization
template<class T>
struct remove_cv<T const> {
    using type = T;
};
template<class T>
struct remove_cv<T volatile> {
    using type = T;
};
template<class T>
struct remove_cv<T volatile const> {
    using type = T;
};

这里通常用 type 来表示应用一个 template 之后的结果。

完整代码

// remove_cv_0.cpp
#include <iostream>
#include <boost/type_index.hpp>
using boost::typeindex::type_id_with_cvr;
using std::cout;
using std::endl;

// primary defination.
template<class T>
struct remove_cv {
    using type = T;
};
// partial specialization
template<class T>
struct remove_cv<T const> {
    using type = T;
};
template<class T>
struct remove_cv<T volatile> {
    using type = T;
};
template<class T>
struct remove_cv<T volatile const> {
    using type = T;
};


int main(int argc, char *argv[])
{
    cout << "remove_cv<int> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    cout << "remove_cv<const int> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    cout << "remove_cv<int const> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    cout << "remove_cv<int volatile> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    cout << "remove_cv<int const volatile> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    return 0;
}

程序输出

remove_cv<int> = int
remove_cv<const int> = int
remove_cv<int const> = int
remove_cv<int volatile> = int
remove_cv<int const volatile> = int

重写 remove_cv

我们可以写简化一下 remove_cv ,这里的 "简化" ,我理解不是“变得更简单" 的意思。而是让他变得更加 “可组合的”。

这个比较简单,直接上代码了。这里不停地重复一个模式,就是上面谈到的模式。

完整代码

// remove_cv_0.cpp
#include <iostream>
#include <boost/type_index.hpp>
using boost::typeindex::type_id_with_cvr;
using std::cout;
using std::endl;

// primary defination.
template<class T>
struct remove_const {
    using type = T;
};
// partial specialization
template<class T>
struct remove_const<T const> {
    using type = T;
};

// primary defination
template<class T>
struct remove_volatile{
    using type = T;
};
// partial specialization
template<class T>
struct remove_volatile<T volatile> {
    using type = T;
};

// bang
template<class T>
struct remove_cv {
    using type = typename remove_const<typename remove_volatile<T>::type >::type;
};


int main(int argc, char *argv[])
{
    cout << "remove_cv<int> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    cout << "remove_cv<const int> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    cout << "remove_cv<int const> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    cout << "remove_cv<int volatile> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    cout << "remove_cv<int const volatile> = " << type_id_with_cvr<remove_cv<int>::type >().pretty_name() << endl;
    return 0;
}

程序输出

remove_cv<int> = int
remove_cv<const int> = int
remove_cv<int const> = int
remove_cv<int volatile> = int
remove_cv<int const volatile> = int

重写 is_void

有了 is_sameremove_cv ,我们可以简化 is_void 的实现。

// primary defination.
template<class T>
struct is_void : public is_same<typename remove_cv<T>::type, void> {
};

完整代码

// is_void_0.cpp
#include <iostream>
using std::cout;
using std::endl;


struct true_type {
    static constexpr bool value = true;
};
struct false_type {
    static constexpr bool value = false;
};

// primary defination.
template<class T, class U>
struct is_same : public false_type {
};

// partial specialization.
template<class T>
struct is_same<T,T> : public true_type {
};
// primary defination.
template<class T>
struct remove_cv {
    using type = T;
};
// partial specialization
template<class T>
struct remove_cv<T const> {
    using type = T;
};
template<class T>
struct remove_cv<T volatile> {
    using type = T;
};
template<class T>
struct remove_cv<T volatile const> {
    using type = T;
};


// primary defination.
template<class T>
struct is_void : public is_same<typename remove_cv<T>::type, void> {
};



int main(int argc, char *argv[])
{
    cout << "is_void<int> = " << is_void<int>::value << endl;
    cout << "is_void<void> = " << is_void<void>::value << endl;
    cout << "is_void<void const> = " << is_void<void const>::value << endl;
    cout << "is_void<void volatile> = " << is_void<void volatile>::value << endl;
    cout << "is_void<void const volatile> = " << is_void<void const volatile>::value << endl;
    return 0;
}

程序输出

is_void<int> = 0
is_void<void> = 1
is_void<void const> = 1
is_void<void volatile> = 1
is_void<void const volatile> = 1

is_one_of 判断一个类是否是某些类中的一个

  1. is_one_of<int>::valuefalse
  2. is_one_of<int,int>::valuetrue
  3. is_one_of<int, float, int>::valuetrue
  4. is_one_of<int, float, double>::valuefalse
  5. is_one_of<int, float, double, int>::valuetrue

这里使用了 c++11 中的可变长模板的特性,但是基本的模板编程模式是不变的。

定义 primary template

// primary defination.
template<class T, class... P0toN>
struct is_one_of : public false_type {
};

定义 partial specialization

// partial specialization.
template<class T, class... P1toN>
struct is_one_of<T,T, P1toN...> : public true_type {
};

这个是显而易见的,如果和列表中的第一个类型相同,那么就是 true_type

如果匹配失败呢,那么我们有一种递归消减的模式。

template<class T, class U, class... P1toN>
struct is_one_of<T, U, P1toN...> : public is_one_of<T, P1toN...> {
};

或者

template<class T, class U, class... P1toN>
struct is_one_of<T, U, P1toN...> {
    static constexpr bool value = is_one_of<T, P1toN...>::value;
};

我不确定哪一种风格是好的。

完整代码

// is_one_of_0.cpp
#include <iostream>
using std::cout;
using std::endl;


struct true_type {
    static constexpr bool value = true;
};
struct false_type {
    static constexpr bool value = false;
};

// primary defination.
template<class T, class... P0toN>
struct is_one_of : public false_type {
};

// partial specialization.
template<class T, class... P1toN>
struct is_one_of<T,T, P1toN...> : public true_type {
};
template<class T, class U, class... P1toN>
struct is_one_of<T, U, P1toN...> : public is_one_of<T, P1toN...> {
};


int main(int argc, char *argv[])
{
    cout << "is_one_of<int> = " << is_one_of<int>::value << endl;
    cout << "is_one_of<int,int> = " << is_one_of<int,int>::value << endl;
    cout << "is_one_of<int,float,int> = " << is_one_of<int,float,int>::value << endl;
    cout << "is_one_of<int,float,double> = " << is_one_of<int,float,double>::value << endl;

    return 0;
}

程序输出

is_one_of<int> = 0
is_one_of<int,int> = 1
is_one_of<int,float,int> = 1
is_one_of<int,float,double> = 0

is_copy_assignable 判断一个类是否可复制,可赋值

如果 T 定义了拷贝构造函数和赋值操作符重载,那么 is_copy_assignable<T>::valuetrue ,否则就是 false 。举几个 false 的例子

  1. is_copy_assignable<unique_ptr<int> >::value
  2. is_copy_assignable<mutex >::value
  3. is_copy_assignable<lock_guard<mutex> >::value

这个实现起来是很有难度的。我们从简单的事情做起。第一步,写 primary template 。

template<typename T>
struct is_copy_assignable : true_type {
}

恩,这个看起来很简单,不是吗?实例化的部分就有难度了。

首先,我们定义一个简单的东西,declval

template<typename T>
T declval();

声明一个模板函数,declval<T>() 的返回值的类型是 T 。这个函数没有定义,我们无法运行期调用这个函数。

编译期,我们可以使用 decltype(declval<T>()) 得到类型 Tdecltype 是 c++11 的特性。

如果一个类定义了赋值操作符重载,那么下面的表达式就是成立的。

decltype( declval<U&>() = declval<U const&>() )

declval<U const&>() 返回一个 U const& 的对象 xdeclval<U&>() 返回一个 U& 的对象 y ,然后试图调用赋值语句,y=x

等等,这里我们并没有真正的执行求值(evaluate) 的动作,无论是编译期还是运行期。decltype 就像 sizeof, noexcepttypeid 一样,并不真正执行求值的动作。一切发生在想象中。

无论如何,如果 U 没有定义 = 操作符重载的话,上面的语句就会失败(failure) ,注意,我没有使用出错(error) 这个表达方式。 SFINAE (Substitue Failure Is Not An Error) 。就是说,如果上面的表达式是不合法的 (ill-formed) ,那么编译器不认为是错误,继续尝试匹配其他的表达式。

template<typename T>
struct is_copy_assignable {
  private:
    template<class U, class = decltype( declval<U&>() = declval<U const&>() )>
    static true_type try_assignment(U&& );

    static false_type try_assignment(...);
  public:
    using type = decltype( try_assignment(declval<T>()));
    static constexpr bool value = type::value;
};

通常,起名字是程序员最头痛的事情,如果你不引用一个东西,就不要给他起名字。例如上面的例子,class = ... 就没有起名字,尽管没有名字,这个模板参数可以有一个默认值。默认值就是那一长串 decltype( declval<U&>() = declval<U const&>()

static false_type try_assignment(...);

这个是一个十分不常用的语法,... ,来自于 C 语言的历史遗产,printf(...) 。 这个在 C++ 中极力不提倡使用,推荐使用可变长模板。原因是这个没有 type safe ,多少 c 语言的 bug 倒在这个上面。 这 C++ 中,可变长的参数的函数,在函数重载(overload) 中是最后一个选项。也就谁说,函数重载 (overload) 的时候,寻找合适的重载函数时候,只有所有其他匹配都不成功的时候,才会匹配这个可变长参数的同名函数。

把两个重载的 try_assigment 连起来看,我们理解一下 SFINAE 。如果 U 定义了 = 操作符重载,那么 decltype( declval<U&>() = declval<U const&>() 就是一个合理的表达式,匹配成功,于是 try_assigment() 的返回值的类型是 true_type 。否则,匹配失败,但是失败不是错误,继续匹配,匹配到了第二个可变长参数及的 try_assigment,这时,返回值的类型是 false_type

如果我们在重复利用 delctypedeclval 的技巧,就可以得到 type 的定义。

using type = decltype( try_assignment(declval<T>()));

后面的 static constexpr bool value 就不难理解了。

这段代码极其不好理解,因为看起来十分的奇怪。这也是为什么 template 让大家觉得陌生。

这段代码并不是最好的,因为用到太多的小技巧,后面 Walter E. Brown 会试图重写一下。

有几个技术是值得学习的。 decltype ,这个十分有用。decltype 很独特,因为他的参数是一个表达式,而不是一个 type ,这个表达式即不在运行期求值,也不在编译器求值。 为了配合使用 decltype ,我们才会引入一个 declval<T>() 的傻函数。 decltype + declval 的技术不是很难,十分有用,应该被掌握。

... 的可变参数,还有函数重载的复杂规则,我们应该敬而远之。尤其是函数重载。考虑到默认构造函数,默认类型转换函数,默认参数,模板函数等等语言特性,模板函数的重载规则是十分复杂的。如果大多数人都记不住这些复杂的规则,那么利用这些规则写出来的代码,就可读性很差了。

这些例子远远没有到达库函数的质量,因为我们没有考虑很多边缘案例,例如,如果 T 有等号操作符重载,但是等号操作符重载函数的返回值不是 T&

完整代码

// is_copy_assignable_0.cpp
#include <iostream>
#include <memory>
#include <mutex>
using std::cout;
using std::endl;


struct true_type {
    static constexpr bool value = true;
};
struct false_type {
    static constexpr bool value = false;
};

template<typename T>
T declval();


// primary defination.
template<typename T>
struct is_copy_assignable {
  private:
    template<class U, class = decltype( declval<U&>() = declval<U const&>() )>
    static true_type try_assignment(U&& );

    static false_type try_assignment(...);
  public:
    using type = decltype( try_assignment(declval<T>()));
    static constexpr bool value = type::value;
};


int main(int argc, char *argv[])
{
    cout << "is_copy_assignable<unique_ptr<int> > = " <<
            is_copy_assignable<std::unique_ptr<int> >::value << endl;

    cout << "is_copy_assignable<mutex> = " <<
            is_copy_assignable<std::mutex>::value << endl;


    cout << "is_copy_assignable<lock_guard> = " <<
            is_copy_assignable<std::lock_guard<std::mutex> >::value << endl;

    cout << "is_copy_assignable<int > = " <<
            is_copy_assignable<int>::value << endl;

    return 0;
}

程序输出

is_copy_assignable<unique_ptr<int> > = 0
is_copy_assignable<mutex> = 0
is_copy_assignable<lock_guard> = 0
is_copy_assignable<int > = 1

void_t 一个奇怪的,而有用的模板类

template<class...>
using void_t = void;

void_t 已经是 c++17 的标准了 http://en.cppreference.com/w/cpp/types/void_t 。 但是大神讲这个视频的时候,还没有进去。

这个类型看起来没啥用,很简单。

你给他无论啥类型,他都返回一个 void 类型。

关键点是,你给他的类型必须是有效的类型 (well-formed),不能是非法的的类型(ill-formed)。

这里还用的了 c++11 的一个新特性, using ,这个十分有用,让代码看起来十分简洁。

后面大神讲到应用这个 void_t 重写 is_copy_assignable 的时候,现场一片掌声。

使用 void_t,编写 has_type_member

如果 T::type 是一个合法的表达式,那么 has_type_member<T>::value 是 true ,否则就是 false 。例如

  1. has_type_member<enable_if<true> >::value = true
  2. has_type_member<int>::value = false

按照模式,我们首先定义 primary template

// primary defination.
template<class T, class = void >
struct has_type_member : public true_type {
};

很明显,大多数我们自定义的类,都没有 T::type 的定义,于是默认返回 false 。

注意 class = void 十分关键。 要理解他为什么十分关键,就需要理解 c++ 是如何找到匹配的 template specialization 的。这个比较长,我简单说一下我的理解。

  1. 首先确定模板参数。
  2. 看用户给定的参数,如果有给定参数,那么用用户提供的参数。
  3. 如果用户没有提供模板参数,那么用默认的。
  4. 找到所有模板参数之后,看看哪一个实例化是更加匹配的,找到最匹配的那一个。

对于 has_type_member<int>来说,has_type_member 的第一个模板参数是 int ,用户提供的,第二个模板参数,用户没有提供,我们用默认的,就是 void ,这就是为什么 class = void` 十分关键。

找到参数之后,这个时候,编译器发现有两个匹配的模板。

部分特例化的 struct has_type_member<T, void_t<typename T::type> > : public true_type ,这个匹配不上,因为 void_t<int::type> 匹配失败。 匹配失败不是错误,继续匹配。匹配到了 primary template 。于是 has_type_member<int>::value 是 false 。

对于 has_type_member<enable_if<true,void> >来说,has_type_member 的第一个模板参数是 enable_if<true,void> ,用户提供的,第二个模板参数,用户没有提供,我们用默认的,就是 void 。

部分特例化的 struct has_type_member<T, void_t<typename T::type> > : public true_type ,这个匹配成功,因为 void_t<int::type> 是合理的表达式。于是 has_type_member<T, void_t<typename T::type>::value 是 true 。

这个模式十分有用,简单易懂。可以判断一个类是否有成员函数,静态成员变量,成员变量等等。我们就离实现 concept 不远了。

完整代码

// has_type_member_0.cpp
#include <iostream>
#include <type_traits>
using std::cout;
using std::endl;


struct true_type {
    static constexpr bool value = true;
};
struct false_type {
    static constexpr bool value = false;
};

// the imfamous void_t
template<class...>
using void_t = void;


// primary defination.
template<class T, class = void >
struct has_type_member : public false_type {
};

// partial specialization.
template<class T>
struct has_type_member<T, void_t<typename T::type> > : public true_type {
};


int main(int argc, char *argv[])
{
    cout << "has_type_member<int> = " << has_type_member<int>::value << endl;
    cout << "has_type_member<std::enable_if<true,void> > = " << has_type_member<std::enable_if<true,void> >::value << endl;
    return 0;
}

程序输出

has_type_member<int> = 0
has_type_member<std::enable_if<true,void> > = 1

重写 is_copy_assignable

应用模式,写出 primary template

// primary defination.
template<typename T, class = void>
struct is_copy_assignable : public false_type {
};

默认都不能 copy assignable 。

然后 partial specialization

// partial specialization
template<typename T>
struct is_copy_assignable<T, void_t<decltype( declval<T&>() = declval<T const&>() )> >
        : public true_type {
};

这里 Brown 赢得的一片掌声。 的确精妙。 他的精妙之处,不在于解决了一个特定的问题,而在于他创造了一种模式,应用这种模式,让人们不再使用那些难懂的技巧,就可以写出可读性很好的模板程序。

完整代码

// is_copy_assignable_1.cpp
#include <iostream>
#include <memory>
#include <mutex>
using std::cout;
using std::endl;


struct true_type {
    static constexpr bool value = true;
};
struct false_type {
    static constexpr bool value = false;
};

template<typename T>
T declval();

// the imfamous void_t
template<class...>
using void_t = void;


// primary defination.
template<typename T, class = void>
struct is_copy_assignable : public false_type {
};

// partial specialization
template<typename T>
struct is_copy_assignable<T, void_t<decltype( declval<T&>() = declval<T const&>() )> >
        : public true_type {
};



int main(int argc, char *argv[])
{
    cout << "is_copy_assignable<unique_ptr<int> > = " <<
            is_copy_assignable<std::unique_ptr<int> >::value << endl;

    cout << "is_copy_assignable<mutex> = " <<
            is_copy_assignable<std::mutex>::value << endl;


    cout << "is_copy_assignable<lock_guard> = " <<
            is_copy_assignable<std::lock_guard<std::mutex> >::value << endl;

    cout << "is_copy_assignable<int > = " <<
            is_copy_assignable<int>::value << endl;

    return 0;
}

程序输出

is_copy_assignable<unique_ptr<int> > = 0
is_copy_assignable<mutex> = 0
is_copy_assignable<lock_guard> = 0
is_copy_assignable<int > = 1

c++ 中的 remove-erase 俗语

Erase–remove_idiom 和 Scott Mayer 的 effective stl 都提到过这个俗语。

类似下面的代码,删除所有偶数。

// remove_erase.cpp
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char *argv[])
{
    vector<int> avec={1,2,3,4,6,7,8};
    avec.erase(remove_if(avec.begin(), avec.end(), [](int a) { return a %2 == 0; }), avec.end());

    for(auto i : avec){
        cout << "i= " << i << endl;
    }
    return 0;
}

我们深入研究一下,remove 之后,vector 里面变成了什么。

// remove_erase_2.cpp
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char *argv[])
{
    vector<int> avec={1,2,3,4,6,7,8};
    auto it = remove_if(avec.begin(), avec.end(), [](int a) { return a %2 == 0; });
    cout << "before erase " << endl;
    for(auto i : avec){
        cout << "i= " << i << endl;
    }

    avec.erase(it, avec.end());

    cout << "after erase " << endl;
    for(auto i : avec){
        cout << "i= " << i << endl;
    }
    return 0;
}
before erase
i= 1
i= 3
i= 7
i= 4
i= 6
i= 7
i= 8
after erase
i= 1
i= 3
i= 7

可以看到, remove 重新排列了 vector 中的元素。

但是后面的元素有些奇怪,2 不见了,出现两个 7 。无论如何,根据手册,后面的元素已经是无效了。我们只是好奇,后面的元素是什么。

我们看看 remove 的实现,gcc 中的 stl 的实现。

template <class _ForwardIterator, class _Predicate>
_ForwardIterator
remove_if(_ForwardIterator __first, _ForwardIterator __last, _Predicate __pred)
{
    __first = std::__1::find_if<_ForwardIterator, typename add_lvalue_reference<_Predicate>::type>
                           (__first, __last, __pred);
    if (__first != __last)
    {
        _ForwardIterator __i = __first;
        while (++__i != __last)
        {
            if (!__pred(*__i))
            {
                *__first = std::__1::move(*__i);
                ++__first;
            }
        }
    }
    return __first;
}

可以看到,标准库里面用 std::move 函数重新排列了顺序。

这里可以看到,代码移动了 37 元素,但是不保证 2 元素继续有效,因为他的位置已经无效了,在移动过程中,2 把位置让给了 33 的位置让给了 7

我们验证一下移动的次数。

// remove_erase_3.cpp
#include <iostream>
using namespace std;
#include <vector>

class Foo {
  public:
    Foo(int v): value(v) {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__<< "] "
             << "v "  << v << " "
             << endl;
    }
    Foo(const Foo& other): value(other.value) {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__<< "] "
             << "other.value "  << other.value << " "
             << "value "  << value << " "
             << endl;
    }
    Foo& operator =(Foo&& other) {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__<< "] "
             << "other.value "  << other.value << " "
             << "value "  << value << " "
             << endl;
        value = other.value;
        other.value = 0;
        return *this;
    }
    ~Foo() {
            cout <<  __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__<< "] "
                 << "value "  << value << " "
                 << endl;
    }
  public:
    int value;
};

using namespace std;
int main(int argc, char *argv[])
{
    vector<Foo> avec={1,2,3,4,6,7,8};
    auto it = remove_if(avec.begin(), avec.end(), [](Foo& a) { return a.value %2 == 0; });
    cout << "before erase " << endl;
    for(auto i : avec){
        cout << "i= " << i.value << endl;
    }

    avec.erase(it, avec.end());

    cout << "after erase " << endl;
    for(auto i : avec){
        cout << "i= " << i.value << endl;
    }
    return 0;
}

我们观察一下输出

remove_erase_3.cpp:9: [Foo] v 1 
remove_erase_3.cpp:9: [Foo] v 2 
remove_erase_3.cpp:9: [Foo] v 3 
remove_erase_3.cpp:9: [Foo] v 4 
remove_erase_3.cpp:9: [Foo] v 6 
remove_erase_3.cpp:9: [Foo] v 7 
remove_erase_3.cpp:9: [Foo] v 8 
remove_erase_3.cpp:14: [Foo] other.value 1 value 1 
remove_erase_3.cpp:14: [Foo] other.value 2 value 2 
remove_erase_3.cpp:14: [Foo] other.value 3 value 3 
remove_erase_3.cpp:14: [Foo] other.value 4 value 4 
remove_erase_3.cpp:14: [Foo] other.value 6 value 6 
remove_erase_3.cpp:14: [Foo] other.value 7 value 7 
remove_erase_3.cpp:14: [Foo] other.value 8 value 8 
remove_erase_3.cpp:29: [~Foo] value 8 
remove_erase_3.cpp:29: [~Foo] value 7 
remove_erase_3.cpp:29: [~Foo] value 6 
remove_erase_3.cpp:29: [~Foo] value 4 
remove_erase_3.cpp:29: [~Foo] value 3 
remove_erase_3.cpp:29: [~Foo] value 2 
remove_erase_3.cpp:29: [~Foo] value 1 
remove_erase_3.cpp:20: [operator=] other.value 3 value 2 
remove_erase_3.cpp:20: [operator=] other.value 7 value 0 
before erase 
remove_erase_3.cpp:14: [Foo] other.value 1 value 1 
i= 1
remove_erase_3.cpp:29: [~Foo] value 1 
remove_erase_3.cpp:14: [Foo] other.value 3 value 3 
i= 3
remove_erase_3.cpp:29: [~Foo] value 3 
remove_erase_3.cpp:14: [Foo] other.value 7 value 7 
i= 7
remove_erase_3.cpp:29: [~Foo] value 7 
remove_erase_3.cpp:14: [Foo] other.value 4 value 4 
i= 4
remove_erase_3.cpp:29: [~Foo] value 4 
remove_erase_3.cpp:14: [Foo] other.value 6 value 6 
i= 6
remove_erase_3.cpp:29: [~Foo] value 6 
remove_erase_3.cpp:14: [Foo] other.value 0 value 0 
i= 0
remove_erase_3.cpp:29: [~Foo] value 0 
remove_erase_3.cpp:14: [Foo] other.value 8 value 8 
i= 8
remove_erase_3.cpp:29: [~Foo] value 8 
remove_erase_3.cpp:29: [~Foo] value 8 
remove_erase_3.cpp:29: [~Foo] value 0 
remove_erase_3.cpp:29: [~Foo] value 6 
remove_erase_3.cpp:29: [~Foo] value 4 
after erase 
remove_erase_3.cpp:14: [Foo] other.value 1 value 1 
i= 1
remove_erase_3.cpp:29: [~Foo] value 1 
remove_erase_3.cpp:14: [Foo] other.value 3 value 3 
i= 3
remove_erase_3.cpp:29: [~Foo] value 3 
remove_erase_3.cpp:14: [Foo] other.value 7 value 7 
i= 7
remove_erase_3.cpp:29: [~Foo] value 7 
remove_erase_3.cpp:29: [~Foo] value 7 
remove_erase_3.cpp:29: [~Foo] value 3 
remove_erase_3.cpp:29: [~Foo] value 1 

我试着解释一下输出结果。

  1. {1,2,3...} 调用 Foo(int) 构造了 7 个 Foo 对象
  2. vector 通过 Foo(const Foo& x) ,把 initialize_list 中的对象拷贝到 vector 中。
  3. 原来的 7 initialize_list 中的对象被析构掉了。
  4. remove 移动对象,调用了 operator=(Foo&&) ,后面具体解释。
  5. 在打印过程中,因为 for(auto i: 中,没有使用 auto& ,于是拷贝构造了临时对象 1, 3, 7, 4, 6, 0, 8 ,然后打印,然后析构这些临时对象
  6. 调用 vector.erase 的时候,会调用 ~Foo 析构掉 8,0,6,4 。可以看到,vector 是倒着析构这些对象的。不过这个无关紧要,标准没有强调析构顺序。
  7. 然后类似步骤 5 打印过程中,构造,析构 临时对象 。 1,3,7
  8. main 函数结束,析构 vector 对象,vector 对象倒着析构了 7,3,1 三个对象。

上面的代码,第4步中,在移动的过程中,打印调试信息,显式 2 的位置让给了 3 。然后 3 的位置设置为无效值 value = 03 位置上已经是带有了无效值,这个位置让给了 7 ,同时 7 的位置被设置成为了无效值。

c++11 的 extern template

c++98 的 template 有些问题,代码会膨胀,而且不容易定位到底是实例化了那一个实例。

我设计了一个例子,展现这两个问题。

// extn_tmpl.hpp
#pragma once

template<typename T>
T foo(T a , T b ){
    return a + b + EXTRA;
}
#ifdef EXTERN_TMPL
extern template int foo<int>(int a, int b);
#endif

这里可以看到,根据预处理的宏定义 EXTRA 的不同,会产生不同的实例。当然,这个例子看起来十分傻,但是在真实的项目中,由于多人开发,项目复杂,这种例子还真有可能会出现。

// extn_tmpl_1.cpp
#include "extn_tmpl.hpp"
int foo_caller1(int a, int b)
{
    return foo(a,b);
}
// extn_tmpl_2.cpp
#include "extn_tmpl.hpp"
int foo_caller2(int a, int b)
{
    return foo(a,b);
}

这里是简单的使用 extn_tmpl.hpp

// extn_tmpl_main.cpp
extern int foo_caller1(int a, int b);
extern int foo_caller2(int a, int b);

#ifdef EXTERN_TMPL
#include "extn_tmpl.hpp"
template int foo(int a, int b);
#endif


int main(int argc, char *argv[])
{
    if(argv[1][0] == '1'){
        return foo_caller1(1,1);
    }else if(argv[1][0] == '2'){
        return foo_caller2(1,1);
    }
    return 0;
}

我们采用下面的命令编译程序

% c++ -std=c++11 -DEXTRA=1 -c -O3 -fno-inline -o extn_tmpl_1.o extn_tmpl_1.cpp
% c++ -std=c++11 -DEXTRA=2 -c -O3 -fno-inline -o extn_tmpl_2.o extn_tmpl_2.cpp
% c++ -std=c++11 -c -O3 -fno-inline -o extn_tmpl_main.o extn_tmpl_main.cpp
% nm extn_tmpl_2.o | c++filt
0000000000000000 T foo_caller2(int, int)
0000000000000010 T int foo<int>(int, int)
% nm extn_tmpl_1.o | c++filt
0000000000000000 T foo_caller1(int, int)
0000000000000010 T int foo<int>(int, int)
% ls -l extn_tmpl_1.o extn_tmpl_2.o
-rw-r--r--  1 wangchunye  staff  760 Apr 29 23:32 extn_tmpl_1.o
-rw-r--r--  1 wangchunye  staff  760 Apr 29 23:32 extn_tmpl_2.o
% c++ extn_tmpl_main.o extn_tmpl_1.o extn_tmpl_2.o
% ./a.out 1
% echo $?
3
% ./a.out 2
% echo $?
3
% c++ extn_tmpl_main.o extn_tmpl_2.o extn_tmpl_1.o
% ./a.out 1
% echo $?
4
% ./a.out 2
% echo $?
4

注意到,extn_tmpl_1extn_tmpl_2 因为 EXTRA 的定义不同,看到了不同的 foo 的实例化。 而实际使用哪一个,完全取决于链接的时候,是 extn_tmpl_1 在前面,还是 extn_tmpl_2 在前面,谁在前面,就用哪一个。

这个太危险了,因为在编译期,我们无法确定最终的可执行文件使用哪一个 foo 的实例化。

还有一个问题,就是 foo<int>()extn_tmpl_1extn_tmpl_2 中都有定义。导致了代码臃肿。

如果我们使用 extern template 的 c++11 的特性,

% c++ -std=c++11 -DEXTERN_TMPL -DEXTRA=1 -c -O3 -fno-inline -o extn_tmpl_1.o extn_tmpl_1.cpp
% c++ -std=c++11 -DEXTERN_TMPL -DEXTRA=2 -c -O3 -fno-inline -o extn_tmpl_2.o extn_tmpl_2.cpp
% nm extn_tmpl_2.o
% c++filt
0000000000000000 T foo_caller2(int, int)
                 U int foo<int>(int, int)
% nm extn_tmpl_1.o | c++filt
0000000000000000 T foo_caller1(int, int)
                 U int foo<int>(int, int)
% ls -l extn_tmpl_1.o extn_tmpl_2.o
-rw-r--r--  1 wangchunye  staff  664 Apr 29 23:38 extn_tmpl_1.o
-rw-r--r--  1 wangchunye  staff  664 Apr 29 23:38 extn_tmpl_2.o
% c++ extn_tmpl_main.o extn_tmpl_1.o extn_tmpl_2.o
Undefined symbols for architecture x86_64:
  "int foo<int>(int, int)", referenced from:
      foo_caller1(int, int) in extn_tmpl_1.o

因为 -DEXTERN_TMPL , extern template 被激活。

这样我们注意到两件事

  1. foo<int> 不再出现在 extn_tmpl_1.o 或者 extn_tmpl_2.o 的定义中,而成为了未定符号。
  2. 文件大小变小了,从 760 字节,变成了 664 字节。

也许你会说,文件变小一点点,没什么大不了,然而,在真实的项目中,有很多 header only 的库,巨大的头文件,被几千个小文件包含,这个代码膨胀的数字就很可观了。

还有,我们在链接的时候,得到了一个链接错误,这正是我们想要的,我们需要确定的知道实例化的实现是什么,如果不知道,那么就抛出错误。

我们可以重新编译 extn_tmpl_main.o ,来定义 foo<int>

% c++ -std=c++11 -DEXTERN_TMPL -DEXTRA=0 -c -O3 -fno-inline -o extn_tmpl_main.o extn_tmpl_main.cpp
% nm extn_tmpl_main.o
% c++filt
                 U foo_caller1(int, int)
                 U foo_caller2(int, int)
0000000000000000 T int foo<int>(int, int)
0000000000000010 T _main

这样,我们确切的知道,foo<int> 定义在 extn_tmpl_main 中, 无论怎样调换 extn_tmpl_1extn_tmpl_2 链接的时候的位置,都得到相同的运行结果。

% c++ extn_tmpl_main.o extn_tmpl_1.o extn_tmpl_2.o
% ./a.out 1
% echo $?
2
% ./a.out 2
% echo $?
2
% c++ extn_tmpl_main.o extn_tmpl_2.o extn_tmpl_1.o
% ./a.out 1
% echo $?
2
% ./a.out 2
% echo $?
2

"配置 haskell 的开发环境"

ghc 官方下载,然后安装,挺顺利的。 我用的是 osx 下的 ghc 8.0.2.

安装 emacs 下的 haskell-mode.el 也十分容易,按照 https://github.com/haskell/haskell-mode 的手册操作,也没有什么难度。

用 emacs 打开一个文件 hello_world.hs ,可以看到安装 haskell-mode 之后,emacs 能够用 haskell-mode 关联 hs 扩展名的文件。

module Main where
main ::  IO ()
main =
  putStrLn "hello world"

然后选择菜单,Haskell -> Start Interpreter ,然后选择,Haskell -> Load File ,这样进入一个 *haskell* 的 buffer 。运行指令

λ> :main
hello world

我们也可以手工加载这个程序,假设,我们的源文件叫做 Main.hs ,那么,我们可以使用 :load 指令加载这个文件。

λ> :load Main.hs
[1 of 1] Compiling Main             ( Main.hs, interpreted )
Ok, modules loaded: Main.
Collecting type info for 1 module(s) ...
λ> main
hello world

Haskell 寻找模块的方式和 Java 类似,模块名称必须大写字母开头,模块名称和源文件名称一致。

-- Foo.hs
module Foo where

foo x y = x + y

运行这个程序

λ> :load Foo.hs
[1 of 1] Compiling Foo              ( Foo.hs, interpreted )
Ok, modules loaded: Foo.
Collecting type info for 1 module(s) ...
λ> Foo.foo 1 2
3

修改 Foo.hs 如下

-- Foo.hs
module Foo where

foo x y = x + y

bar x y = x * y

我们可以用 :reload 指令重新加载模块。

λ> :reload
[1 of 1] Compiling Foo              ( Foo.hs, interpreted )
Ok, modules loaded: Foo.
Collecting type info for 1 module(s) ...
λ> Foo.bar 2 4
8

每次都输入 Foo. 也挺麻烦的,我们可以用 :module 指令简化这个操作

λ> :module +Foo
λ> foo 2 3
5
λ> bar 3 3
9

"c/c++ 的编译和链接的问题"

介绍

hello world in c 中,提到了一些编译链接 的问题。在实际项目中,大家学习很多 c/c++ 的语言特性,而忽略了编译和链 接的过程。 对编译和链接过程的理解,反过来有助于我们理解 c/c++ 的很多语 言特性。

前提

为了让观点更加明显,我去除所有的库函数调用。这样就需要理解 main 函数的返回值。

我们写一个最简单的程序

// simple_main.cpp
int main(int argc, char * argv[]) {
   return 12;
}

我们看一下程序输出

% c++ simple_main.cpp && ./a.out ; echo $?
12

在 bash 中 $? 表示进程的返回值。也就是 main 函数的返回值。

编译过程

我们看下面的程序。这个程序在编译过程中不会报错的。

extern int foo(int input);
int main(int argc, char * argv[]) {
   return foo(12);
}

注意到,我为了排除噪音,没有任何 #include 语句。

% c++ -c -o simple_main.o simple_main.cpp

命令成功执行,没有任何错误。-c 表示编译。如果不带这个选项, gcc 自动 启动连接过程,会有链接错误。

-o 表明输出文件的位置。我们检查一下输出文件。

linux 下运行

% nm -C simple_main.o

OSX 下运行

% nm  simple_main.o
                 U __Z3fooi
0000000000000000 T _main

% nm  simple_main.o | c++filt
                 U foo(int)
0000000000000000 T _main

这里我们注意到, U 表示引用外部符号。这就是 extern int foo(int input) 的作用。

我们还注意到,c++ 的 mangle/demangle 的作用个, int foo 被翻译成了 __z3fooi 了,这种程序在链接过程中, C 语言的程序是无法找到这个符号的。

我们换一种方法

extern "C" int foo(int input);
int main(int argc, char * argv[]) {
   return foo(12);
}
% g++ -c -o simple_main.o simple_main.cpp
% nm simple_main.o
                 U _foo
0000000000000000 T _main

这里我们看到 extern "C" 的作用就是防止 mangle 。

参考 https://en.wikipedia.org/wiki/Name_mangling

这里不同的平台会自动添加一个下划线在符号中。我们先忽略这个。

编译成功。

链接简单的目标文件

// foo1.cpp
int foo(int input){
    return input;
}
// foo2.cpp
int foo(int input){
    return input*2;
}

我们分别编译一下。

% c++ -c -o foo1.o foo1.cpp
% c++ -c -o foo2.o foo2.cpp
% nm foo1.o
0000000000000000 T __Z3fooi
% nm foo2.o
0000000000000000 T __Z3fooi

nm 的输出中, T 表示这个函数已经在这个目标文件中定义了。

如果我们链接第一个库

% c++ -o a.out simple_main.o foo1.o && ./a.out ; echo $?

12

% c++ -o a.out simple_main.o foo2.o && ./a.out ; echo $?

24

可以看到,只有连接的时候,才能决定我们调用哪一个函数。如果我们声明的是 extern "C" int foo(int) 的话,就会有连接错误。

Undefined symbols for architecture x86_64:
  "_foo", referenced from:
      _main in simple_main.o

解决这个错误的办法就是用 C 语言写一个 foo 函数。

// foo1.c
int foo(int i) { return i*3; }
% gcc -c -o fooc.o  foo1.c
% nm fooc.o
0000000000000000 T _foo

% c++  -o a.out fooc.o simple_main.o
% ./a.out ; echo $?
36

在连接阶段,语言的特征就比较模糊了,连接器看到就是一大堆没有定义的符号,无论这些目标文件是 c, c++ 还是汇编语言。还是其他什么语言生成的目标文件。

尽管如此,符号上还是带有一些语言特征,例如 c++ 的 mangle 机制。

如果我们两个目标文件都链接的话,会产生链接错误。

5 g++ -o a.out simple_main.o foo1.o foo2.o
duplicate symbol __Z3fooi in:
    foo1.o
    foo2.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

链接静态链接库

静态链接库很简单,就是一大堆目标文件的堆叠。

创建链接库

% ar r libfoo1.a foo1.o
% ar r libfoo2.a foo2.o

连接静态链接库

% c++ -L. -o a.out simple_main.o -lfoo1

-L. 表明到哪里寻找 libfoo.a 。这里用当前目录。

-lfoo1 表示要连接 foo1 这个库。

如果我们两个库都链接的话

% g++ -o a.out simple_main.o -L. -lfoo1 -lfoo2
% ./a.out; echo $?
12
% g++ -o a.out simple_main.o -L. -lfoo2 -lfoo1
% ./a.out; echo $?
24

和链接目标文件不一样,如果出现符号重复定义,这里没有任何错误,具体调用哪一个 foo 函数,完全取决于 -l 在链接过程中出现的位置。

这是一个经常误入的一个坑点,注意,这里和编译没有关系。完全是连接的问题。

链接共享链接库

shared library 的使用,必须重新编译所有目标文件,让目标文件是位置无关的代码 PIC, Position Independent Code。

% g++ -c -fPIC -o simple_main.o  simple_main.cpp
% g++ -c -fPIC -o foo1.o foo1.cpp
% g++ -c -fPIC -o foo2.o foo2.cpp

生成动态链接库

OSX 下

% g++ -dynamiclib -o libfoo1.dylib foo1.o
% g++ -dynamiclib -o libfoo2.dylib foo2.o

linux 下

% g++ -o libfoo1.so -shared foo1.o
% g++ -o libfoo2.so -shared foo2.o

链接

% g++ -L. -o a.out simple_main.o -lfoo1
% ./a.out ; echo $?
12
% g++ -L. -o a.out simple_main.o -lfoo2
./a.out ; echo $?
24

链接不同的库,调用不同的动态链接库。

同时链接两个

% g++ -L. -o a.out simple_main.o -lfoo1 -lfoo2
./a.out; echo $?
12
% g++ -L. -o a.out simple_main.o -lfoo2 -lfoo1
./a.out; echo $?
24

和静态链接库类似,符号重复定义的时候,没有报错,取决于链接命令中,库出现的顺序。

延伸,故事才开始,共享链接和静态连接有很多很多的不同。换句话说,静态链接很简单,几乎和直接链接目标文件一样。动态链接就不一样了。还取决于生成期链接和运行期链接。 rpath, LD_LIBRARY_PATH 等等。比较复杂。这里指出其复杂性,不做展开讨论。

链接动态链接库

这里指出 windows 下的 DLL 是动态链接库,和 *unix 的共享链接库有很多类似,但也有很多不同。我在 windows 下的经验有限,不是十分了解。仅仅指出他们不同。

"c++ non copyable 的传递性"


什么是 non copyable 的类

标准中有些类很有意思,是 non-copyable 的,不可拷贝的。 例如 unique_ptr, mutex, lock_guard 。 这些类的拷贝构造函数标记称为删除了。

例如 lock_guard

lock_guard( const lock_guard& ) = delete;	(3) 	(since C++11)

这些 non-copyable 的类,他们的对象中,无法有 copy 的语义。于是,通常来说,他们的赋值操作符重载也被标记为删除了。

... operator=(...) deleted; // not copy-assignable

这些语义上就是不可拷贝的对象,在实际中有很多应用。

这种语义是有传递性的,也就是说,如果一个类的成语变量是一个 non-copyable 的,那么这个类本身也应该是一个 non-copyable 的。例如

#include <iostream>
#include <memory>
using namespace std;


class Foo {
  public:
  private:
    unique_ptr<int> pInt_;
};


int main(int argc, char *argv[])
{
    Foo a;
    Foo b{a};

    return 0;
}

这个代码会有编译错误

simple_non_copyable.cpp:16:9: error: call to implicitly-deleted copy constructor of 'Foo'
    Foo b{a};
        ^~~~
simple_non_copyable.cpp:9:21: note: copy constructor of 'Foo' is implicitly deleted because field 'pInt_' has a deleted copy constructor
    unique_ptr<int> pInt_;
                    ^

解决这个问题的方法就是,不能使用拷贝构造函数,c++ 在防止作者出现这种设计上的语义错误。

"ssh 的无密码登陆"


无密码登陆

公钥就是锁,私钥就是钥匙。我们可以生成一对钥匙和锁,然后把给服务器上锁,把钥匙留着笔记本上,然后就可以无密码登陆了。

生成钥匙和锁

% ls -l ~/.ssh
total 0
% ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/wcy123/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/wcy123/.ssh/id_rsa.
Your public key has been saved in /home/wcy123/.ssh/id_rsa.pub.
The key fingerprint is:
2b:03:51:b3:86:8c:44:9a:49:fe:ee:ff:0f:ef:77:d6 wcy123@hostname
The key's randomart image is:
+--[ RSA 2048]----+
| oo   o          |
|o= o o o         |
|+.. + o          |
|  .  o           |
|   ..   S        |
|  .  .   .       |
|   .  o..     .  |
|  .    oo  . o E |
|   .....o+. o    |
+-----------------+
% ls -l ~/.ssh                                                                                                   1 ↵
total 16
-rw------- 1 wcy123 ubuntu 1679 Apr  8 09:44 id_rsa
-rw-r--r-- 1 wcy123 ubuntu  396 Apr  8 09:44 id_rsa.pub

上面是在 linux 的笔记本上, windows 上的 putty 或者其他工具也是类似的。

id_rsa 是钥匙, id_rsa.pub 是锁。

给服务器上锁

% cat ~/.ssh/id_rsa.pub | ssh wangchunye@10.10.0.102 'chmod +w $HOME/.ssh/authorized_keys ; tee -a $HOME/.ssh/authorized_keys ; chmod 600 $HOME/.ssh/authorized_keys'
wangchunye@10.10.0.102's password:
chmod: cannot access '/home/wangchunye/.ssh/authorized_keys': No such file or directory
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDIPBvlQ0sfpPFmSsgi2odj/dS9GlaiYCXZOKKm9R2t4RpxWbQafUR7OmcGbNdJNBUyrauEKaH3v1Incaz//PAJ9zalABqBPwc7qzWZfwDYQ3ojsbQMO/mherkI5ZSMYhbnQCAt0k1KqDbZnmS6icAarXpVAvY1nHVWpya/FuepTFp/fPpoq3VN3BlBeX9F4KpeO5V329dKzZs3CCez5q7Woltdr4pwz6BHW8ddoBuQlueKrRru/86rAQxz/CYG5F0QgtTEqoyIcWHVpf01BayeB3vSnKH0URFkBrRTBbY0880XVa5U0skrYc7/tDUG/fIXdjvX8iIDMQGz3RnqtCp1 wangchunye@wangchunye.local

这个命令看起来有些复杂,有些机器上有 ssh-copy-id 可以完成一样的功能。

% ssh-copy-id wangchunye@10.10.0.102

每次 ssh-agent 是本地的一个守护进程,他记录了本地的钥匙。ssh-add 用于添加删除这些钥匙

% ssh-agent
....

%

这样登陆主机就不用密码了。

随身带着钥匙

假设你配置好了笔记本 N, 然后登陆主机 A ,不用密码了,很好。然后从 A 登陆 B ,这个时候你还是需要输入密码。

你可以把 ~/.ssh/id_rsa 也就是你的钥匙,拷贝到主机 A 的 HOME 目录下,但是,这样明显有问题。主机 A 上的管理员账号 root 可以看你的钥匙,偷你的钥匙。

有一个简单的办法

ssh -A hostA

-A 这样登陆 hostA 之后,再登陆其他主机,就不用再次输入密码了,因为 hostB 会问 hostA 上的 ssh 进程,钥匙是啥,ssh 进程再问笔记本上的 ssh-agent 进程,钥匙是啥。ssh-add 已经加进去了钥匙,在你安全的笔记本上。所以这个办法很好。

tmux

我们都知道 tmux 是一个好东西。 配合使用 tmux attach 的时候,上面wangchunye@wangchunye:~ » ssh-agent SSH_AUTH_SOCK=/var/folders/_g/t1l2srf912b7yg4qnw8k5jr00000gn/T//ssh-CS0a1Qh3satx/agent.95237; export SSH_AUTH_SOCK; SSH_AGENT_PID=95238; export SSH_AGENT_PID; echo Agent pid 95238;的连续穿梭的办法就有问题了。第一次在 hostA 上创建 tmux 会话的时候,可以不用密码穿梭到 hostB 上,然后我们 tmux attach 重新连接的时候,就还需要输入密码了,原因是进程不能共享一些环境变量。下面的方法可以搞定这个问题。

stackoverflow

#!/bin/sh
SSHVARS="SSH_CLIENT SSH_TTY SSH_AUTH_SOCK SSH_CONNECTION DISPLAY"

for var in ${SSHVARS} ; do
  echo "export $var=\"$(eval echo '$'$var)\""
done 1>$HOME/.ssh/latestagent

tmux attach 之后,运行下面的命令就可以继续无密码穿梭了。

source  ~/.ssh/saveagent

这个命令很长,可以把这个放到 ~/.bashrc

alias s='source ~/.ssh/saveagent'

然后,打一个 s 就可以继续穿梭了。

"c++ GSL 中的 owner"

owner 是一个底层机制,用于标记一个指针的是所指对象的所有者。

foo(owner<int*> p) {
    delete p; // OK
}

正常来讲,用户层面的代码不会使用这个 owner ,这个东西主要为了静态分析工具来分析资源泄露。

GSL 中, owner 的定义很简单。

template <class T>
using owner = T;

这里用了 c++11 的一个新功能, using 做 type 别名。

Effective Modern C++ 中建议尽量使用 using 来替代 typedef 。

主要有两个好处

  1. 支持模板。
  2. 更加准确

为什么说更加准确?例如

template<typename T>
struct owner {
    typedef T type;
};

这个看起来是不错的。但是使用的时候,需要 owner<int*>::type p ,这样做是有坑的。 没有人阻止客户可以写下下面的代码

template<>
struct owner<int*> {
   typedef float* type;
};
template<>
struct owner<float*> {
};

第一种情况是,完全定义了一个不相关的类型, owner<int*>::type 其实是 float*

第二种情况是,完全没有 type 这个定义,会产生编译错误,抱怨 owner<float*> 在实例化的过程中,::type 没有定义。

回到这个 owner 上,我们看到,owner<T*> 其实就是 T* 的别名而已。很简单,很巧妙的写法。

但是,这个技巧不是给普通应用程序的代码使用的,而是给静态分析工具使用的。

c++ 的 const reference extend lifetime rvalue

c++ 中,如果一个函数返回一个对象,那么这个对象是如果返回的呢?

本文用 OSX 下的 llvm 来汇编理解这个定义。

// life_expansion.cpp
#include <iostream>
#include <functional>
#include <boost/type_index.hpp>
using namespace std;
class Foo {
  public:
    Foo(int v):i(v) {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<< "] "
             << "this "  << this << " "
             << "i "  << i << " "
             << endl;
    }
    Foo(const Foo& foo):i(foo.i + 1) {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<< "] "
             << "i "  << i << " "
             << endl;
    }
    ~Foo() {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<< "] "
             << "i "  << i << " "
             << "this "  << this << " "
             << endl;
        i = -100;
    }
  public:
    int i = 0;
};

Foo calc()
{
    Foo foo(1);
    return foo;
}
void show(const Foo& f){
    printf("%d\nq", f.i);
}
int main(int argc, char *argv[])
{
    printf("before\n");
    {
        const auto& f1 = calc();
        show(f1);
    }
    printf("after\n");
    return 0;
}

我们看一下,产生的汇编

% c++ -O3 -fno-inline -std=c++11 -o a.out life_expansion.cpp
otool -tV a.out | c++filt

截取其中的汇编代码

_main:
        pushq   %rbp
        movq    %rsp, %rbp
        pushq   %rbx
        pushq   %rax
        leaq    0x869(%rip), %rdi ## literal pool for: "before"
        callq   0x100000d10 ## symbol stub for: _puts # 这里调用 printf
        leaq    -0x10(%rbp), %rbx                     # 这里申请临时变量的内存
        movq    %rbx, %rdi
        callq   calc() ## calc()
        movq    %rbx, %rdi
        callq   show(Foo const&) ## show(Foo const&)
        movq    %rbx, %rdi
        callq   Foo::~Foo() ## Foo::~Foo()
        leaq    0x848(%rip), %rdi ## literal pool for: "after"
        callq   0x100000d10 ## symbol stub for: _puts
        xorl    %eax, %eax
        addq    $0x8, %rsp
        popq    %rbx
        popq    %rbp
        retq
        nopw    %cs:(%rax,%rax)
calc():
        pushq   %rbp
        movq    %rsp, %rbp
        pushq   %rbx
        pushq   %rax
        movq    %rdi, %rbx
        movl    $0x1, %esi
        callq   Foo::Foo(int) ## Foo::Foo(int)
        movq    %rbx, %rax
        addq    $0x8, %rsp
        popq    %rbx
        popq    %rbp
        retq
        nopl    (%rax)

这里可以看到,对于 calc 的返回值,实际分配内存是在调用者的堆栈里申请的。

而如果调用者,用一个 const reference 抓住(引用) 这个变量的话,那么这个临时变量的生命周期会被拉长。

他的生命周期拉长到和 const reference 的生命周期一样。

详见 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/1993/N0345.pdf

有了c++11 的 unique_ptr,也许就不应该再使用 new/delete 关键字了

Widget* BuildWidget() {
    Widget * ret = new SomeWidget();
    ...
    return ret;
}

上面是以前 c++ 经常的风格。有了 c++11 的 unique_ptr 或许我们应该这么写

std::unique_ptr<Widget> BuildWidget() {
    std::unique_ptr<Widget>  ret = std::make_unique<Widget>(a1,a2);
    ...
    return ret;
}

也许有人担心性能,其实 unique_ptr 和一个 raw pointer 是同样的大小。这么做几乎没有任何额外开销。

这样做的最大好处是明确所有权。 C++ 区别于其他语言的重要特点就是资源管理。RAII Resource acquisition is initialization 。 于是 new 了一个对象,谁拥有这个对象就变得十分重要了。 返回 unique_ptr 明确的说明,所有权转移给了调用者。这里指释放资源,资源不仅仅是内存。

就算调用者拒绝拥有这个对象,也不会产生资源泄露

std::unique_ptr<Widget> pWidget = BuildWidget();

pWidget 被析构的是时候,会自动释放资源。

很可惜,make_unique 不在 c++11 中,我们可以很容易自己写一个。

template<typename T, typename ...Args>
std::unique_ptr<T> make_unique(Args &&...args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

这么做还有一个好处,就是针对异常友好。就是说在程序某处 throw exception 的时候,不会产生资源泄露。

std::unique_ptr 也可以放在成员变量中。例如

class A {
...
std::unique_ptr<B> pB;
...
};

class A {
...
B* pB;
...
};

相比之下,unique_ptr 当然是有好处的。

还有一种方案,就是A 对象直接拥有一个 b 对象,而不是指针,例如下面这种情况,或许更合适。

class A {
...
B b;
...
};

"c++ 的 universal reference"

估计很多人都没有听说过 universal reference ,这个不奇怪,因为是 Scott Meyers 自己创造的这个术语,参考 https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers 。 C++ 标准里面没有这个术语,标准术语是 Reference collapsing 。但是 这种解释是更难理解,尽管更加准确。

Herb Sutter 同意这个东西应该有一个名字,但是应该叫做 forwarding reference。 因为 universal reference 似乎建议这个东西是更加通用的东西,到处都可以用,其实不是这样。 forwarding reference 则强调 这个东西只有在做 perfect forwarding 的时候才用。 关于 perfect forwarding ,看这里

我理解 scott meyers 的文章的意思只是指当看起来像右值引用的 T&& 出现在 template 的中,他就是 universal reference ,既不是 lvalue reference 也不是 rvalue reference

universal reference 的实际效果就是,你给他一个 lvalue reference 的时候,他就是 lvalue reference ,你给他 rvalue reference 的时候,他就是 rvalue reference 。

看下面的例子

#include <iostream>
#include <functional>
#include <boost/type_index.hpp>
using namespace std;

#define SHOW_TYPE_AND_SIZE(expr) if(1){             \
        cout << #expr << ":\n"; show_type_and_size(expr);   \
}while(0)


template<typename T>
void show_type_and_size(T&& x) {
    cout << "T = "
         << boost::typeindex::type_id_with_cvr<T>().pretty_name() << ";\n"
         << boost::typeindex::type_id_with_cvr<decltype(x)>().pretty_name() << " x;\n"
         << "sizeof(x) "  << sizeof(x) << "\n"
         << endl;
}
int foo() {
    return 100;
}
int main(int argc, char *argv[])
{
    int x = 100;
    int&& rf = foo();
    SHOW_TYPE_AND_SIZE(100);
    SHOW_TYPE_AND_SIZE(rf);
    SHOW_TYPE_AND_SIZE(foo());
    SHOW_TYPE_AND_SIZE(x);
    return 0;
}

输出如下:

100:
T = int;
int&& x;
sizeof(x) 4

rf:
T = int&;
int& x;
sizeof(x) 4

foo():
T = int;
int&& x;
sizeof(x) 4

x:
T = int&;
int& x;
sizeof(x) 4

这里需要注意,尽管 rf 的值是一个 rvalue reference ,但是他本身是一个 lvalue 。参考 C++11 的右值引用问题

c++ lambda capture by value 的实验

c++ 中 lambda 的类型和大小 到 lambda 生成一个匿名类。当没有 capture 任何变量的时候,大小几乎是 0 。

c++11 中的 lambda 关键字对于程序的结构影响很大,其中的闭包是一个很关键 的概念。因为 c++ 语言本身不支持垃圾回收,所以 capture by value 和其他 语言还是有区别的。关键的问题在于,拷贝构造函数调用多少次?

例如代码

#include <iostream>
#include <functional>
#include <boost/type_index.hpp>
using cnamespace std;
class Foo {
  public:
    Foo():i(100) {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<< "] "
             << "i "  << i << " "
             << endl;
    }
    Foo(const Foo& foo):i(foo.i + 1) {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<< "] "
             << "i "  << i << " "
             << endl;
    }
    ~Foo() {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<< "] "
             << "i "  << i << " "
             << endl;
        i = -100;
    }
  public:
    int i = 0;
    friend ostream& operator<<(ostream& out, const Foo & obj);
};
ostream& operator<<(ostream& out, const Foo & obj)
{
    out << "foo(" << &obj << "," << obj.i << ")";
    return out;
}
void
CreateLambda() {
    Foo foo;
    {
      [foo]() { cout << foo.i << endl; }();
    }
}
int main(int argc, char *argv[])
{
    CreateLambda();
    return 0;
}

程序的输出如下

cpp_lambda_capture_1.cpp:8: [Foo::Foo()] i 100 
before create lambda1
cpp_lambda_capture_1.cpp:13: [Foo::Foo(const Foo &)] i 101 
after create lambda1
before create lambda2
cpp_lambda_capture_1.cpp:13: [Foo::Foo(const Foo &)] i 102 
after create lambda2
before create lambda3
cpp_lambda_capture_1.cpp:13: [Foo::Foo(const Foo &)] i 103 
after create lambda3
before create lambda3
cpp_lambda_capture_1.cpp:13: [Foo::Foo(const Foo &)] i 104 
after create lambda4
before lambda4 out of scope
cpp_lambda_capture_1.cpp:18: [Foo::~Foo()] i 104 
after lambda4 out of scope
cpp_lambda_capture_1.cpp:18: [Foo::~Foo()] i 103 
after lambda3 out of scope
before lambda2 out of scope
cpp_lambda_capture_1.cpp:18: [Foo::~Foo()] i 102 
after lambda2 out of scope
cpp_lambda_capture_1.cpp:18: [Foo::~Foo()] i 101 
cpp_lambda_capture_1.cpp:18: [Foo::~Foo()] i 100 

我们仔细看输出,可以看到一下几点

  1. 生成的匿名 lambda 类,内部有一个不可见的成员变量 Foo
  2. 匿名 lambda 对象赋值的时候,会调用内部成员变量变量的 Foo 的拷贝构造函数。
  3. 匿名 lambda 对象析构的时候,会调用内部成员变量的析构函数,析构掉这个不可见的成员变量。

我们看看生成对象的大小

例如代码

#include <iostream>
#include <functional>
#include <boost/type_index.hpp>
#include <iostream>
#include <functional>
#include <boost/type_index.hpp>
using namespace std;

class Foo {
  public:
    Foo():i(100) {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<< "] "
             << "i "  << i << " "
             << endl;
    }
    Foo(const Foo& foo):i(foo.i + 1) {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<< "] "
             << "i "  << i << " "
             << endl;
    }
    ~Foo() {
        cout <<  __FILE__ << ":" << __LINE__ << ": [" << __PRETTY_FUNCTION__<< "] "
             << "i "  << i << " "
             << endl;
        i = -100;
    }
  public:
    int i = 0;
    friend ostream& operator<<(ostream& out, const Foo & obj);
};
ostream& operator<<(ostream& out, const Foo & obj)
{
    out << "foo(" << &obj << "," << obj.i << ")";
    return out;
}


template<typename T>
void show_type_and_size(T& x) {
    cout << "T = "
         << boost::typeindex::type_id_with_cvr<T>().pretty_name() << ";\n"
         << boost::typeindex::type_id_with_cvr<decltype(x)>().pretty_name() << " x;\n"
         << "sizeof(x) "  << sizeof(x) << "\n"
         << endl;
}

void
CreateLambda() {
    Foo foo;
    auto lambda1 =  [foo](){ cout << foo.i << endl;};

    show_type_and_size(foo);
    show_type_and_size(lambda1);

}
int main(int argc, char *argv[])
{
    CreateLambda();
    return 0;
}

程序的输出如下

cpp_lambda_capture_2.cpp:12: [Foo::Foo()] i 100 
cpp_lambda_capture_2.cpp:17: [Foo::Foo(const Foo &)] i 101 
T = Foo;
Foo& x;
sizeof(x) 4

T = CreateLambda()::$_0;
CreateLambda()::$_0& x;
sizeof(x) 4

cpp_lambda_capture_2.cpp:22: [Foo::~Foo()] i 101 
cpp_lambda_capture_2.cpp:22: [Foo::~Foo()] i 100 

可以看到,对象的大小就是 4 字节 ,和 Foo 本身的对象是一样大的。这说明

  1. 除了抓住的对象的内存,匿名 lambda 类本身不占用额外的内存
  2. 匿名 lambda 类没有虚函数表

这里注意到一点

std::function<void(void)> f = [](){...};
auto lambda = [](){...};

这两个是不一样的, f 是一个 std::function ,内部可以包含一个 lambda 对象。会调用 lambda 对象的拷贝构造函数,构造一个新的 lambda 对 象。于是,capture by value 的 lambda 对象,也会调用每一个 capture 对象 的拷贝构造函数。

c++ 中 lambda 的类型和大小

本文说明 c++11 lambda 生成一个匿名类,当没有 capture 到任何变量的时候, 大小几乎为零。其实是 1 byte ,因为 c++ 不允许 0 字节的结构体。

#include <iostream>
#include <functional>
#include <boost/type_index.hpp>
using namespace std;

template<typename T>
void show_type_and_size(T& x) {
    cout << "T = "
         << boost::typeindex::type_id_with_cvr<T>().pretty_name() << ";\n"
         << boost::typeindex::type_id_with_cvr<decltype(x)>().pretty_name() << " x;\n"
         << "sizeof(x) "  << sizeof(x) << "\n"
         << endl;
}

int main(int argc, char *argv[])
{
    auto x = []() { cout << "hello world 1" << endl; };
    auto y = []() { cout << "hello world 2" << endl; };
    show_type_and_size(x);
    show_type_and_size(y);

    function<void(void)> fx = x;
    show_type_and_size(fx);

    return 0;
}

程序的输出结果

T = main::$_0;
main::$_0& x;
sizeof(x) 1

T = main::$_1;
main::$_1& x;
sizeof(x) 1

T = std::__1::function<void ()>;
std::__1::function<void ()>& x;
sizeof(x) 48

这里可以看到,每一个 lambda 有自己的匿名类,main::$_0

如果我们试图创建这样的对象

    decltype(x) x1;

会有报错

cpp_src/cpp_lambda_size_type.cpp:21:17: error: no matching constructor for initialization of 'decltype(x)' (aka '(lambda at cpp_src/cpp_lambda_size_type.cpp:16:14)')
    decltype(x) x1;
                ^
cpp_src/cpp_lambda_size_type.cpp:16:14: note: candidate constructor (the implicit copy constructor) not viable: requires 1 argument, but 0 were provided
    auto x = []() { cout << "hello world" << endl; };
             ^
cpp_src/cpp_lambda_size_type.cpp:16:14: note: candidate constructor (the implicit move constructor) not viable: requires 1 argument, but 0 were provided
1 error generated.

这说明这个匿名类是没有默认构造函数的,但是可以创建引用

    decltype(x) & x1 = x;
    x1();
    decltype(y) & y1 = y;
    y1();

而把 lambda 表达式赋值给一个 function 对象的时候,这个对象的大小就是 48 bytes 了。这是因为调用了

template< class F >
function::function( F f );

这个构造函数。

从这个角度上看,直接使用 lambda ,并且配合使用 auto, decltype ,可以得到更加紧凑的函数对象。

c++ 模板的类型推导

#include <iostream>
#include <boost/type_index.hpp>
using namespace std;

#define S(expr) do{ cerr << #expr << ":"  ; expr; } while(0)

template<typename T>
void foo1(T x) {
    cerr << "T = "
         << boost::typeindex::type_id_with_cvr<T>().pretty_name() << ";"
         << boost::typeindex::type_id_with_cvr<decltype(x)>().pretty_name() << " x;"
         << endl;
}

template<typename T>
void foo2(T& x) {
    cerr << "T = "
         << boost::typeindex::type_id_with_cvr<T>().pretty_name() << ";"
         << boost::typeindex::type_id_with_cvr<decltype(x)>().pretty_name() << " x;"
         << endl;
}

template<typename T>
void foo3(const T& x) {
    cerr << "T = "
         << boost::typeindex::type_id_with_cvr<T>().pretty_name() << ";"
         << boost::typeindex::type_id_with_cvr<decltype(x)>().pretty_name() << " x;"
         << endl;
}

class Widget {
  public:
    Widget() {}
};

int main(int argc, char *argv[])
{
    Widget w1;
    Widget& rw = w1;
    const Widget cw;
    const Widget& crw1 = w1;
    const Widget& crw2 = cw;
    Widget w2 = cw;
    // Widget& w3 = cw;

    S(foo1(w1));
    S(foo2(w1));
    S(foo3(w1));

    cerr << endl;

    S(foo1(rw));
    S(foo2(rw));
    S(foo3(rw));

    cerr << endl;

    S(foo1(cw));
    S(foo2(cw));
    S(foo3(cw));

    cerr << endl;

    S(foo1(crw1));
    S(foo2(crw1));
    S(foo3(crw1));


    return 0;
}

输出结果是

foo1(w1):T = Widget;Widget x;
foo2(w1):T = Widget;Widget& x;
foo3(w1):T = Widget;Widget const& x;

foo1(rw):T = Widget;Widget x;
foo2(rw):T = Widget;Widget& x;
foo3(rw):T = Widget;Widget const& x;

foo1(cw):T = Widget;Widget x;
foo2(cw):T = Widget const;Widget const& x;
foo3(cw):T = Widget;Widget const& x;

foo1(crw1):T = Widget;Widget x;
foo2(crw1):T = Widget const;Widget const& x;
foo3(crw1):T = Widget;Widget const& x;

这里面我们注意到

  • rww1 的推导是完全一样的。也就是说,在模板类型推导的时候,首先忽略到引用。这也是很自然的,引用就是变量的别名,所以他们两个是一样的。同样道理,cwcrw1 的输出也是一样的。
  • 对于 cw ,带有 const 修饰符的,那么就需要模式匹配,也就是把 T 换成 Widget 或者 Widget const ,看哪一个和 Widget const 匹配,哪一个就是 T 的类型。

其他几个不关键的点

  • 宏展开的时候, #expr 可以展成字符串。 stringify
  • 必须指定 Widget() {} 构造函数,否则 const Widget cw 会报错。为什么会报错呢,参考 stack overflow
  • boost::typeindex::type_id_with_cvr 是在运行期打印类型信息的可靠工具。

使用 cmake 管理项目

介绍

cmake 是 c/c++ 的一个项目管理工具。远古人类使用 make, 后来慢慢用 automake/autoconf/libtool 这些工具。 后来有人重新轮了几个,包括 scon, gyp , cmake。

gyp 是 google 的 JavaScript 引擎 V8 的管理工具。据说 google 自己也不用了,我没有确认过。

cmake 一开始是 kde 的项目的管理工具,后来越来越多的人使用了。

hello world

cmake 需要 CMakeLists.txt 作为输入。目录结构如下

├── CMakeLists.txt
└── hello.cpp

CMakeLists.txt 如下

project(hello C CXX)
cmake_minimum_required(VERSION 3.8)
add_executable(hello hello.cpp)

cmake 建议工程生成的文件不要和源代码混在一起,所以我们需要手工创建工程的输出目录,然后用 cmake 生成 Makefiles

% mkdir build
% cd build
% cmake ..
-- The CXX compiler identification is AppleClang 8.0.0.8000042
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/wangchunye/d/working/test_cmake/build
% ./hello
% make
Scanning dependencies of target hello
[ 50%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
% ./hello
hello world
%

hello.cpp 如下

#include <iostream>
using namespace std;

int main(int argc, char *argv[])
{
    cout << "hello world" << endl;
    return 0;
}

生成一个库

假设我们要生成一个静态连接库 libfoo.a

foo.cpp 如下

#include <iostream>
void foo() {
    std::cout << "hello from foo library" << std::endl;
}

我们修改 CMakeLists.txt

project(hello CXX)
cmake_minimum_required(VERSION 3.2)
add_executable(hello hello.cpp)
add_library(foo foo.cpp)
% make
Scanning dependencies of target foo
[ 25%] Building CXX object CMakeFiles/foo.dir/foo.cpp.o
[ 50%] Linking CXX static library libfoo.a
[ 50%] Built target foo
Scanning dependencies of target hello
[ 75%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
% ls -la
total 96
drwxr-xr-x   8 wangchunye  staff    272 Mar 18 11:43 .
drwxr-xr-x   6 wangchunye  staff    204 Mar 18 11:43 ..
-rw-r--r--   1 wangchunye  staff  11732 Mar 18 11:43 CMakeCache.txt
drwxr-xr-x  15 wangchunye  staff    510 Mar 18 11:43 CMakeFiles
-rw-r--r--   1 wangchunye  staff   5756 Mar 18 11:43 Makefile
-rw-r--r--   1 wangchunye  staff   1254 Mar 18 11:43 cmake_install.cmake
-rwxr-xr-x   1 wangchunye  staff  15204 Mar 18 11:43 hello
-rw-r--r--   1 wangchunye  staff   7968 Mar 18 11:43 libfoo.a

使用一个库

修改 hello.cpp 如下

#include <iostream>
using namespace std;
extern void foo();
int main(int argc, char *argv[])
{
    foo();
    return 0;
}

在 CMakeLists.txt 里面添加一行

target_link_libraries(hello foo)

然后就 hello.cpp 就可以使用外部的库了

指定特定的头文件搜索目录

c++ 编译的时候需要搜索头文件,例如,有的时候,我们需要添加搜索头文件目录。 我们修改 hello.cpp 如下

#include <iostream>
using namespace std;
#include "foo.h"
int main(int argc, char *argv[])
{
    foo();
    return 0;
}

创建 include/foo.h 如下

#pragma once
extern void foo();

修改 CMakeLists.txt ,增加搜索目录

include_directories(${CMAKE_SOURCE_DIR}/include)

这里注意到我们使用了 ${CMAKE_SOURCE_DIR} 这个变量。还有很多这样的变量。

  • CMAKE_BINARY_DIR 工程的输出目录
  • CMAKE_CURRENT_BINARY_DIR 当前的 CMakeList.txt 的输出目录, 因为可以包含子工程,这个就是子工程的输出目录。
  • CMAKE_CURRENT_LIST_FILE 当前 CMakeList.txt 的全路径名称
  • CMAKE_CURRENT_LIST_DIR 当前 CMakeList.txt 所在目录
  • CMAKE_CURRENT_SOURCE_DIR 当前的项目源代码目录

太多了,https://cmake.org/Wiki/CMake_Useful_Variables 下有详细的说明

添加第三方库依赖

通常,c/c++ 添加第三方库需要解决两个问题

  • 头文件在哪里?
  • 库文件在哪里?

我们先手工添加一个库。

假设 OpenCV 的头文件安装在了

/usr/local/Cellar/opencv/2.4.13.2/include/opencv/

库文件在

/usr/local/Cellar/opencv/2.4.13.2/lib

那么我们写一个简单的 OpenCV 的程序。

// cv1.cpp
#include "opencv2/highgui/highgui.hpp"
#include <iostream>

using namespace cv;
using namespace std;

int main( int argc, const char** argv )
{
     Mat img = imread(argv[1], CV_LOAD_IMAGE_UNCHANGED); //read the image data in the file "MyPic.JPG" and store it in 'img'

     if (img.empty()) //check whether the image is loaded or not
     {
          cout << "Error : Image cannot be loaded..!!" << endl;
          //system("pause"); //wait for a key press
          return -1;
     }

     namedWindow("MyWindow", CV_WINDOW_AUTOSIZE); //create a window with the name "MyWindow"
     imshow("MyWindow", img); //display the image which is stored in the 'img' in the "MyWindow" window

     waitKey(0); //wait infinite time for a keypress

     destroyWindow("MyWindow"); //destroy the window with the name, "MyWindow"

     return 0;
}

修改 CMakeLists.txt

include_directories("/usr/local/Cellar/opencv/2.4.13.2/include")
link_directories("/usr/local/Cellar/opencv/2.4.13.2/lib")
add_executable(cv1 cv1.cpp)
target_link_libraries(cv1
  -lopencv_calib3d
  -lopencv_contrib
  -lopencv_core
  -lopencv_features2d
  -lopencv_flann
  -lopencv_gpu
  -lopencv_highgui
  -lopencv_imgproc
  -lopencv_legacy
  -lopencv_ml
  -lopencv_nonfree
  -lopencv_objdetect
  -lopencv_ocl
  -lopencv_photo
  -lopencv_stitching
  -lopencv_superres
  -lopencv_ts
  -lopencv_video
  -lopencv_videostab
  )

然后 make 就可以了。

cmake 不建议这样手工的方式添加第三方依赖,很明显,如果第三方库放在其他目录下,就找不到了。而且一长串 -l 也是很累人的。

按照 http://docs.opencv.org/2.4/doc/tutorials/introduction/linux_gcc_cmake/linux_gcc_cmake.html 的描述,建议的方法是

FIND_PACKAGE( OpenCV REQUIRED )
IF(OpenCV_FOUND)
   MESSAGE("Found OpenCV")
   MESSAGE("Includes: " ${OpenCV_INCLUDE_DIRS})
ENDIF(OpenCV_FOUND)
ADD_EXECUTABLE(cv1 cv1.cpp)
TARGET_LINK_LIBRARIES(cv1 ${OpenCV_LIBS})

但是,我们运行 cmake ... 的时候,会报错。

% mkdir build ; cd build; cmake ..
Re-run cmake no build system arguments
-- The CXX compiler identification is AppleClang 8.0.0.8000042
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Error at CMakeLists.txt:10 (find_package):
  By not providing "FindOpenCV.cmake" in CMAKE_MODULE_PATH this project has
  asked CMake to find a package configuration file provided by "OpenCV", but
  CMake did not find one.

  Could not find a package configuration file provided by "OpenCV" with any
  of the following names:

    OpenCVConfig.cmake
    opencv-config.cmake

  Add the installation prefix of "OpenCV" to CMAKE_PREFIX_PATH or set
  "OpenCV_DIR" to a directory containing one of the above files.  If "OpenCV"
  provides a separate development package or SDK, be sure it has been
  installed.


-- Configuring incomplete, errors occurred!
See also "/Users/wangchunye/d/working/test_cmake/build/CMakeFiles/CMakeOutput.log".

提示的很明确,在 CMAKE_MODULE_PATH 中,我们没有 找到 OpenCVConfig.cmake 。 这里可以看到 cmake find_package 的机制, 他实际上是找 <LIBNAME>Config.cmake 然后运行里面的脚本。 cmake 自带了很多这样的脚本, 通常在 /usr/local/share/cmake/Modules 下面。 可惜 OpenCV 并不包含在里面, OpenCVConfig.cmake 在 OpenCV 自己的目录下。

/usr/local/Cellar/opencv/2.4.13.2/share/OpenCV/OpenCVConfig.cmake

所以我们运行下面的命令可以解决这个问题

cmake  -DOpenCV_DIR=/usr/local/Cellar/opencv/2.4.13.2/share/OpenCV ..

OpenCVConfig.cmake 文件的开头有文档,说明了使用方法,例如,哪些变量可以使用。

再来一个使用 boost 的例子。 下面这些代码,也是在 FindBoost.cmake 的开头可以找到。

FIND_PACKAGE(Boost 1.36.0 COMPONENTS locale)
IF(Boost_FOUND)
  MESSAGE("Found Boost")
  MESSAGE("BOOST Includes: " ${Boost_INCLUDE_DIRS})
  MESSAGE("BOOST Libraries directories: " ${Boost_LIBRARY_DIRS})
  MESSAGE("BOOST Libraries: " ${Boost_LIBRARIES})
ENDIF(Boost_FOUND)
INCLUDE_DIRECTORIES(${Boost_INCLUDE_DIRS})
ADD_EXECUTABLE(hello_boost hello_boost.cpp)
TARGET_LINK_LIBRARIES(hello_boost ${Boost_LIBRARIES})
// hello_boost.cpp
//
//  Copyright (c) 2009-2011 Artyom Beilis (Tonkikh)
//
//  Distributed under the Boost Software License, Version 1.0. (See
//  accompanying file LICENSE_1_0.txt or copy at
//  http://www.boost.org/LICENSE_1_0.txt)
//
#include <boost/locale.hpp>
#include <iostream>

#include <ctime>

int main()
{
    using namespace boost::locale;
    using namespace std;
    generator gen;
    locale loc=gen("");
    // Create system default locale

    locale::global(loc);
    // Make it system global

    cout.imbue(loc);
    // Set as default locale for output

    cout <<format("Today {1,date} at {1,time} we had run our first localization example") % time(0)
          <<endl;

    cout<<"This is how we show numbers in this locale "<<as::number << 103.34 <<endl;
    cout<<"This is how we show currency in this locale "<<as::currency << 103.34 <<endl;
    cout<<"This is typical date in the locale "<<as::date << std::time(0) <<endl;
    cout<<"This is typical time in the locale "<<as::time << std::time(0) <<endl;
    cout<<"This is upper case "<<to_upper("Hello World!")<<endl;
    cout<<"This is lower case "<<to_lower("Hello World!")<<endl;
    cout<<"This is title case "<<to_title("Hello World!")<<endl;
    cout<<"This is fold case "<<fold_case("Hello World!")<<endl;

}

// vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4

运行的时候需要指定 BOOST_ROOT

-DBOOST_ROOT=/usr/local/Cellar/boost/1.63.0/

使用 googletest 做测试

首先安装 googletest

% mkdir $HOME/opt
5 cd $HOME/opt
% git clone https://github.com/google/googletest.git
% cd googletest
% mdkir build
% cmake -DCMAKE_INSTALL_PREFIX=/usr/local/googletest ..
% make
5 make install

可以看到 CMAKE_INSTALL_PREFIX 用于指定安装目录, make install 用于安装。

类似的,我们可以找 FindGTest.cmake ,文件开头有文档,介绍怎么使用。

ENABLE_TESTING()
FIND_PACKAGE(GTest REQUIRED)
ADD_EXECUTABLE(test1 test1.cpp)
TARGET_LINK_LIBRARIES(test1 GTest::GTest GTest::Main)
ADD_TEST(all_tests test1)

这里 ENALBE_TESTING() 启动 cmake 的集成测试功能。 ADD_TEST 用来添加一个测试用例。

// test1.cpp
#include <iostream>
#include <cmath>
using namespace std;
#include "gtest/gtest.h"
double square_root (const double x)
{
    return sqrt(x);
}
TEST (SQUARE_ROOT_TEST, PositiveNos) {
    ASSERT_NEAR (18.0, square_root (324.0), 1.0e-4);
    ASSERT_NEAR (25.4, square_root (645.16), 1.0e-4);
    ASSERT_NEAR (50.3321, square_root (2533.310224),1.0e-4);
}

运行上面的例子

% cmake -DGTEST_ROOT=/usr/local/googletest ..

添加源代码依赖关系

由于某种原因,需要添加第三方的源代码依赖。我们可以用 ExternalProject_Add 来完成这个功能。

假设我们使用 protobuf 的第三方库。

INCLUDE(ExternalProject)
ExternalProject_Add(
  googleprotobuf_proj
  GIT_REPOSITORY "http://gitlab.i.deephi.tech/wangchunye/protobuf.git"
  SOURCE_SUBDIR "cmake"
  CMAKE_ARGS "-Dprotobuf_BUILD_TESTS=OFF"
  GIT_TAG "v3.2.0"
  UPDATE_COMMAND ""
  INSTALL_COMMAND ""
)
ExternalProject_Get_Property(googleprotobuf_proj SOURCE_DIR)
ExternalProject_Get_Property(googleprotobuf_proj BINARY_DIR)
include_directories("${SOURCE_DIR}/src")
link_directories("${BINARY_DIR}")
set(Protobuf_PROTOC_EXECUTABLE ${BINARY_DIR}/protoc)
  • INCLUDE(ExternalProject) 引入 cmake 脚本。 否则 ExternalProject_Add 不能使用
  • ExternalProject_Add 引入外部第三库
  • googleprotobuf_proj 是第三方库的项目名称,我们自己随便取的的,建议不要和现有的库名称混,否则名字冲突,例如 protobuf 就不行。
  • GIT_TAG 这个指明 sha1 ,branch name 或者 tag name 。 的锁定特定的软件版本。
  • SOURCE_SUBDIR 一般不需要指定,因为 CMakeLists.txt 一般都在项目的根目录里面,但是 probobuf 是放在了 cmake 的子目录下,所以需要指定这个目录。这个功能貌似只有 cmake 3.2 以上的版本才支持。
  • CMAKE_ARGS 指明在子项目中运行 cmake 时,cmake 的命令行参数。
  • -Dprotobuf_BUILD_TESTS=off 关闭测试。否则 protobuf 需要依赖 gmock 无法编译成功,除非安装 google mock 。但是一般我们也不想运行测试。
  • ExternalProject_Get_Property 临时引入变量。
  • include_directories("${SOURCE_DIR}") 使用引入的变量
  • set 设置我们自己的变量

我们定义自己的项目,使用 protobuf

ADD_CUSTOM_COMMAND(
  OUTPUT
  "${CMAKE_CURRENT_BINARY_DIR}/msg.pb.cc"
  "${CMAKE_CURRENT_BINARY_DIR}/msg.pb.h"
  COMMAND  ${Protobuf_PROTOC_EXECUTABLE}
  ARGS --cpp_out  ${CMAKE_CURRENT_BINARY_DIR} --proto_path ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/msg.proto
  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/msg.proto
  COMMENT "Running C++ protocol buffer compiler on ${FIL}"
  VERBATIM )
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR})
ADD_EXECUTABLE(hello_proto hello_proto.cpp "${CMAKE_CURRENT_BINARY_DIR}/msg.pb.cc")
TARGET_LINK_LIBRARIES(hello_proto  -lprotobuf)
add_dependencies(hello_proto googleprotobuf_proj)
  • ADD_CUSTOM_COMMAND 是底层 cmake 命令, 自定义 makefile 的规则,用于从 proto 文件生成 cpp 源码。
  • 注意生成的源代码被放在了 --cpp_out ${CMAKE_CURRENT_BINARY_DIR} 下面,这是一个好习惯,不要让中间文件污染源代码目录了。
  • add_dependencies 比较重要,这个表明需要先下载编译 protobuf ,然后才能编译 hello_proto

其他常用功能

  • 调试 cmake
% export VERBOSE=1
% make

这样可以输出详细的编译命令,我们可以检查编译选项是否正确。

  • 添加自定义宏
add_definitions("-DFOO=\" foo libray name \"")

对应的,修改 foo.cpp

#include <iostream>

void foo() {
 std::cout << "hello from " << FOO << "library" <<  std::endl;
}
% make
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/wangchunye/d/working/test_cmake/build
[ 50%] Built target foo
Scanning dependencies of target hello
[ 75%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
% ./hello
hello from foo library

使用 google test framework

本文参考 http://www.ibm.com/developerworks/aix/library/au-googletestingframework.html

下载 google test

如果使用 cmake

include(ExternalProject)
ExternalProject_Add(
  googletest
  GIT_REPOSITORY "https://github.com/google/googletest"
)
include_directories("${SOURCE_DIR}/googletest/include")
link_directories("${BINARY_DIR}/googlemock/gtest/")
add_executable(test1
  ${CMAKE_CURRENT_LIST_DIR}/test1.cpp)
target_link_libraries(test1 gtest gtest_main)

test1.cpp 的内容

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include <functional>
#include <deque>
#include <cassert>
#include <cmath>
using namespace std;

#include "gtest/gtest.h"


double square_root (const double x)
{
    return sqrt(x);
}
TEST (SQUARE_ROOT_TEST, PositiveNos) {
    ASSERT_NEAR (18.0, square_root (324.0), 1.0e-4);
    ASSERT_NEAR (25.4, square_root (645.16), 1.0e-4);
    ASSERT_NEAR (50.3321, square_root (2533.310224),1.0e-4);
}

可用的宏

  • ASSERT_FLOAT_EQ
  • ASSERT_DOUBLE_EQ
  • EXPECT_NEAR
  • EXPECT_EQ
  • ASSERT_DEATH
  • ASSERT_EXIT

todo

understand fixtures

google c++ style by examples

详细参考 https://google.github.io/styleguide/cppguide.html

文件名

my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
myusefulclass_test.cc // _unittest and _regtest are  deprecated.

类名

class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTableProperties *, string> PropertiesMap;

// using aliases
using PropertiesMap = hash_map<UrlTableProperties *, string>;

// enums
enum UrlTableErrors { ...

变量名

int a_local_variable;
int a_struct_data_member;
int a_class_data_member_;
string tableName; // 不好

成员变量

class TableInfo {
  ...
 private:
  string table_name_;  // OK - underscore at end.
  string tablename_;   // OK.
  static Pool<TableInfo>* pool_;  // OK.
};
struct UrlTableProperties {
  string name;
  int num_entries;
  static Pool<UrlTableProperties>* pool;
};

常量

const int kDaysInAWeek = 7;

函数名

  • good

    void StartRpc() {}
    
  • bad

    void StartRpc() {}
    

缩写不需要

TERMINAL 下的快捷操作

改变 shell

一般我们登陆的 shell 是 bash 。安装 oh-my-zsh 可以使用 zsh 作为默认的 shell 。 zsh 和 bash 类似,zsh 交互性更好一些。

readline 下的快捷键

很多 terminal 下的程序,包括 bash , zsh 等等,都使用了 libreadline 这个库,这个库提供一些命令行下的编辑功能。

按键含义
C-a移动到行首
C-e移动到行尾
C-kcut 到行尾
C-yyank
M-yyank more
C-@开始选择文章
M-f移动到下一个单词
M-b移动到上一个单词
C-r在命令历史里面向前搜索

yank 就是 “粘贴”。 readline 有一个自己的剪切板。和系统剪切板没有任何关系。 远程登录到服务器上,也可以使用 cut&paste 的功能。

readline 有多个剪切板, M-y 可以在最近的剪切板里面选择。

每个进程有自己独立的剪切板,之间互不干扰。

小叮当效应

本文参考 https://www.youtube.com/watch?v=fhoxNgJvTMw

随机笔记

小叮当效应

当越来越多的人相信某件事是真的,那么这件事慢慢就变成真的了。

例如:“钱”。美国人相信美国政府一时半会儿不会倒逼,所以越来越多的人相信,”美元“有价值。 “美元”是可以买水,食品,武器,燃料的。那么”美元”真的看上去就是有价值的。

反小叮当效应

当越来越多的人相信某件事是真的,那么这件事慢慢就变成假的了。

例如,”开车很安全”,越多的人相信,人们开车就很随意了,开车的人也多了,其实慢慢的,实际上 “开车就不安全了”。

例如,“你的选票很重要”,越多的人相信,人们就开始去投票,因为我的投票很重要。其实慢慢的,实际上,”你的选票很不重要”。

存款准备金的意义

本文参考 https://www.youtube.com/watch?v=fhoxNgJvTMw

随机笔记

存款准备金的意义

我们都知道,经济生活中流通的钱越多,我们口袋里的钱就越不值钱了。这是通过膨胀。存款准备金也有类似的效果。

假设张三在建设银行存了 100 元钱。假设存款准备金是 10% ,银行不用存储那么多钱,只要留下 10 元钱,应付有人来取现金。剩下的 90 元,银行可以随意支配,例如,借给李四。于是,这个时候,李四想从银行借钱,李四就可以借走 90 元。

这个时候,张三有 100 元可以支配,张三花钱的时候基本上是刷卡,其实银行只是张三账户调整了数字而已。

李四有 90 元可以支配,李四借的钱也不是现金,花钱的时候也是刷卡,其实银行只是调整了张三账户里面的数字。

社会上本来只有 100 元,因为银行的借贷,导致社会上流动的钱,变成了 190 元钱了。

如果降低存款准备金,社会上流动的就变多了,你的存款就缩水了。如果提高存款准备金,你的存款就升值了。

过拟合和正则化

本文参考 http://neuralnetworksanddeeplearning.com/chap3.html

过拟合和正则化(Overfitting and regulation)

模型中自由参数的个数

通常来说,自由变量的个数越多,模型的表达能力越强。例如下面的一些原始测量点。

假设我们有两种模型,一种是九次多项式模型

\( y = a_0 + a_1 x + a_2 x^2 + \cdots + a_9 x^9 \)

另一种是简单的线性模型,即,一次多项式模型

\( y = a_0 + a_1 x \)

我们得到下面两个结果。

九次模型的拟合结果

一次模型的拟合结果

很明显,9次模型比1次模型的表达能力更强,“表达能力更强”是一个不严谨的说法,意味着这个模型可以记住所有的训练数据。9次模型有 10 个自由参数,可以“记住”更多的训练数据。如果严格数学上描述“表达能力更强”,我理解是指,这个模型可以把成本函数优化到很低很低,也就是说,能很好的拟合所有训练数据。

如果我们真正使用训练好的模型,用模型来预测未知的数据,这时我们很难说“表达能力强”的模型的预知能力更好。

具体哪个模型更好,其实是没有答案的,取决于哪一个建模更加接近真实的模型。假设上面的测量数据,就是一个线性模型的测量结果,那么1次模型,就是更好的建模。通常,如果没有特别的原因,我们倾向使用更简单的模型。

测量的原始数据是有测量误差的,而9次模型严格拟合了所有训练数据,把测量误差也当做真实模型的有用信息了。这就是过拟合。

建模的过程中,自由参数 (free parameters) 的个数是一个很重要的量。就算一个模型可以很好的拟合训练数据,但如果自由参数的数量不够的话,那么这个模型也有可能不是一个很好的模型。自有参数的个数太多也不好,因为会出现过拟合的现象。因为他有可能只是说只针对训练数据有效。真正使用的时候,就哑火了。

这种过拟合,就是说成本函数看起来已经很小了,似乎模型训练的很好,但是实际的预测率却上不去。

训练样本的数量和自由参数的个数

过拟合的本质是过度拟合了噪声,或者测量误差。而不是现实模型的本质。 所以,有的时候,成本函数变得很小,区分率并没有得到提高,原因是我们过分匹配了训练数据,而训练数据中,我们无法区分测量误差和噪声,这样就导致我们的模型似乎只是记住了训练数据,只有针对训练数据表现的很好,但是碰到了真实数据,就哑火了。

解决这个问题的办法就是提高训练样本的个数,让训练样本的个数远远超过自有参数的个数。就像上面的例子,如果有1000 个测量点,如果真实数据的确是线性关系的话,我相信 9 次模型也能收敛到 1次模型上。

实际上,训练数据很贵或者很难得到。而且训练样本增多,训练时间也变得很长很长。

但是当训练样本个数小于或者接近自有参数的个数的时候,过拟合就问题就很明显了。

精确建模,从而减少自有参数的个数

通过精确的建模,我们也可以防止过拟合的问题,模型中的自由参数都是精准设计的。

精确建模减少了自有参数的个数,不仅解决了过拟合的问题,同时也提高了模型的预测能力。

实际上,这个也是不切实际的,有些问题是如此的复杂,以至于我们几乎无法做精确的建模。

正则化

正则化 (regulation) 是一种减少过拟合的方法。

\(L^2\) 正则化,权值退化

我们盯着本质模型仔细看一会儿

\[ z = w \hat a + b \]

下一级的输入是上一级输出的线性变换。假设我们的测量数据 $\hat a$ 中有我们不可知的误差,假设是加性的误差,那么 \( \hat a = a + e \)

\[ z = w(a+e) + b \]

可以看到 \(w\) 放大了这种误差,无论这个误差是加性的,还是其他。

如果我们假定 \(w\) 很小,这样就减少了放大误差的可能性。

体现真相的 \(a\) 会在很多训练数据中重复出现,所有的 \(w\) 都很小,其实本质上不影响我们的模型训练,只不过是同比例放大缩小。

体现误差的 \(e\) 对于每一个训练数据都不一样,某一个单一的训练样本的误差,不会对训练模型有很大影响。

注意这里面没有偏移量啥事,因为偏移量没有和测量误差没有关系。

\(L^2\) 正则化,在成本函数中增加一个新的项。

\[ \frac{\lambda}{2n} \sum_w w^2 \]

\(\lambda\) 是正则化参数。

这一项说明,权值越大,成本越大。可见,我们希望有绝对值很小的权值。除非我们有大量的样本 $n$ ,否则我们希望权值越小越好。

可以看到,我们希望用很小的权值来最小化的成本函数的同时,\(\lambda\) 用于寻找一种平衡,是减小权值更重要呢?还是最小化成本函数最重要?

L1 正则化

\[ \frac{\lambda}{n} \sum_w |w| \]

L1 的正则化看上去和 L2 十分类似,设计初衷也是一样的,区别是 L1 和 L2 的权值退化程度不一样。 L1 的权值退化速度是一个常量,而且向 0 逼近。而 L2 的权值退化速度和 \(w\) 成正比,\(w\) 越大,退化速度越大。

这样 L1 的效果是,网络模型中会留下少数几个比较重要的权重。而其他不重要的权重,都趋近于 0 了。

dropout 正则化

dropout 正则化不是修改成本函数的表达方式,他修改神经网络本身。

上面的讨论中,我们可以看到,减少自由参数的个数,让他和训练样本的个数差不多,或者远远小于训练样本的个数,这样就可以减少过拟合。

dropout 的方法利用这一点,每次训练权值的时候,随机选取一部分神经元,这样做之后,导致每次训练权值的时候,其实模型中的参数个数没有那么多。

每次训练都是随机选取模型中的一个子集,这样某一特定样本就不会对整个模型产生十分大的影响。

Back Propagation 算法的向量表示

back propagation 中描述的是按照单个元素,本文试图用向量和矩阵表示模型。

神经网络建模

\[ \begin{align} z_{l,j} &= b_{l,j} + \sum_{i=0}^{N_{l-1} -1 } w_{l-1,j,i} a_{l-1,i} \\ a_{l,j} &= \sigma(z_{l,j}) \end{align} \]

其中

  • \(L\) 为神经网络的层数,包含输入层,包含输出层。上面的例子中是 4 层神经网络
  • \(l = 0,...,L-1\) 是层的下标
  • \(N_{l}\) 是第 \(l\) 层神经网络的神经元的个数
  • \(j = 1, ..., N_{l}\) 每一次的第 \(j\) 个神经元的下标
  • \(z_{l,j}\) 是第 \(l\) 层神经网络中,第 \(j\) 个神经元的 sigmoid 函数的输入。
  • \(a_{l,j}\) 是第 \(l\) 层神经网络中,第 \(j\) 个神经元的 sigmoid 函数的输出。特别的,当 \(l=0\) 时, \(a_{0,i}\) 表示输入层,\(N_0\) 表示输入层的个数。
  • sigmoid 函数可以是,例如 \[\sigma(x) = \frac{1}{1+e^{-x}}\]
  • \(w_{l-1,j,i}\) 是第 \(l-1\) 层中第 \(i\) 个输出和 \(l\) 层的第 \(j\) 个输入之间的权值。
  • \(b_{l,i}\) 是第 \(l\) 层中第 \(j\) 个神经元的偏移量

重新用向量和矩阵定义模型

\[ \begin{align} \vec{z}_l &= \vec{b}_l + \matrix{W}_{l-1} \vec{a}_{l-1}\\ \vec{a}_{l} &= \sigma(\vec{z}_{l} ) \\ \vec{a}_{l-1} &= \sigma(\vec{z}_{l-1} ) \end{align} \]

其中

  • \(\vec{z}_l\) 是第 \(l\) 层神经元的输入向量。
  • \(\vec{a}_l\) 是第 \(l\) 层神经元的输出向量。
  • \(\matrix{W} _ {l-1}\) 是第 \(l-1\) 层神经元和 \(l\) 层神经元之间的权重矩阵是 \(N_{l} \times N_{l-1}\) 维矩阵
  • \(N_{l}\) 是第 \(l\) 层神经元的个数
  • \(N_{l-1}\) 是第 \(l-1\) 层神经元的个数
  • \(\vec{b}_l\) 是第 \(l\) 层神经元的偏移量向量。
  • sigmoid 函数可以是,例如 \[\sigma(x) = \frac{1}{1+e^{-x}}\]
  • \(\sigma({\vec{x}})\) 表示应用函数 \(\sigma\) 到向量 \(\vec{x}\) 的每一个元素上,产生的一个新的同维向量。

back propagation

目的

给定一个训练样本 \(\vec{a}_{0}\), 和期待的输出 \(\vec{y}\) 。

\(\vec{a}_{0}\), 表示输入层的神经元产生的输出。

定义成本函数如下

\[ \begin{align} C &= \frac{1}{2} (\vec{y} - \vec{a} _ {L-1})^T \cdot (\vec{y} - \vec{a} _ {L-1})\ &= \frac{1}{2} <(\vec{y} - \vec{a} _ {L-1}), (\vec{y} - \vec{a} _ {L-1}) > \end{align} \]

其中

  • \(\vec{x}^T\) 表示向量的转置。
  • \(\vec{x} \cdot \vec{y}\) 表示向量的内积。
  • \(\vec{a}_ {L-1}\) 表示选取了一组 \(\matrix{W}_{l}\) 和 \(\vec{b}_l\) 之后,\(l=0...L-1\) ,输出层神经元的输出。

解决方案

梯度的定义

\[ \begin{align} \frac{\partial{C}}{\partial{\matrix{W} _ {l-1}}} &= \left(\begin{array}{cccc} \frac{\partial{C}}{\partial{w _ {l-1,0,0}}} & \frac{\partial{C}}{\partial{w _ {l-1,0,1}}} & \cdots & \frac{\partial{C}}{\partial{w _ {l-1,0,N _ {l-1}-1}}} \ \frac{\partial{C}}{\partial{w _ {l-1,1,0}}} & \frac{\partial{C}}{\partial{w _ {l-1,1,1}}} & \cdots & \frac{\partial{C}}{\partial{w _ {l-1,1,N _ {l-1}-1}}} \ \vdots & \ddots & & \vdots \ \frac{\partial{C}}{\partial{w _ {l-1,N _ {l}-1,0}}} & \frac{\partial{C}}{\partial{w _ {l-1,N _ {l}-1,1}}} & \cdots & \frac{\partial{C}}{\partial{w _ {l-1,N _ {l}-1,N _ {l-1}-1}}} \end{array}\right) \ \frac{\partial{C}}{\partial{\vec{b} _ {l}}} &= \left( \begin{array}{c} \frac{\partial{C}}{\partial{b _ {l,0}}} \ \frac{\partial{C}}{\partial{b _ {l,1}}} \ \vdots \ \frac{\partial{C}}{\partial{b _ {l,N _ {l} - 1}}} \end{array} \right) \end{align} \]

其中

  • \(N _ {l}\) 是第 \(l\) 层神经元的个数
  • \(N _ {l-1}\) 是第 \(l-1\) 层神经元的个数

梯度降低的方法,随机选取一组答案,然后沿着梯度方向,即 \(\frac{\partial{C}}{\partial{\matrix{W} _ {l-1}}}\) 和 \(\frac{\partial{C}}{\partial{\vec{b} _ {l}}}\) ,减小。 逐步找到合适的答案,\(\matrix{W} _ {l-1}\) 和 \(\vec{b} _ {l}\) 。

那这里的问题就是,如何求 \(\frac{\partial{C}}{\partial{\matrix{W} _ {l-1}}}\) 和 \(\frac{\partial{C}}{\partial{\vec{b} _ {l}}}\) ?

推导

如果定义

\[\vec{\delta} _ {l}= \frac{\partial{C}}{\partial{\vec{z} _ {l}}}\]

\(\vec{\delta} _ {l}\) 的含义就是,如果神经元输入 \(\vec{z} _ {l}\) 变化一点点, 会导致成本函数 \(C\) 变化多少?(TODO: 应该用 \(\triangledown\) 表示,否则有歧义,见下面)

因为

\[ \vec{z} _ l = \vec{b} _ l + \matrix{W} _ {l-1} \vec{a} _ {l-1} \]

根据链式求导法则,就得出了下面的公式。

\[ \begin{align} \frac{\partial{C}}{\partial{W _ {l-1}}} &= \vec{\delta} _ {l} \vec{a} _ {l-1} ^T \end{align} \]

其中

  • \(\vec{\delta} _ {l}\) 是一个 \(N _ {l} \times 1\) 的向量。
  • \(\vec{a} _ {l-1}\) 是一个 \(N _ {l-1} \times 1\) 的向量。
  • \(\matrix{W}\) 是一个 \(N _ {l} \times N _ {l-1}\) 的矩阵。

类似的

\[ \begin{align} \frac{\partial{C}}{\partial{\vec{b} _ {l}}} &= \vec{\delta} _ {l} \end{align} \]

那么现在的关键是如何求 \(\vec{\delta} _ {l}\)

思路类似于自然归纳法,就是先求 \(\vec{\delta} _ {L-1}\) ,然后假设已知所有 \(\vec{\delta} _ {l}\) ,如果求 \(\vec{\delta} _ {l-1}\) 。

首先考虑第 \(L - 1\) 层,即最后一层。

\[ \begin{align} \vec{\delta} _ {L-1} &= \frac{\partial{C}}{\partial{\vec{z} _ {L-1}}} \end{align} \]

根据链式求导法则

\[ \begin{align} \delta _ {L-1,j} &= \frac{\partial{C}}{\partial{\vec{a} _ {L-1}}} \otimes \sigma'(\vec{z} _ {L-1}) \ &= \left( \triangledown _ {\vec{a} _ {L-1}}C \right) \otimes \sigma'(\vec{z} _ {L-1}) \end{align} \]

其中

  • \(\otimes\) 表示向量元素两两相乘,得到的新向量。
  • \(\frac{\partial{C}}{\partial{\vec{a} _ {L-1,j}}}\) 表示针对向量 \(\vec{a}\) 的每一个元素求偏导数。也表示成 \(\triangledown _ a\) (TODO: 表示成 \(\triangledown _ a\) 更合适,因为前者有歧义,针对向量的方向导数也是这么表示的,后面还是尽量用 \(\triangledown _ a\) 表示)

这样,我们得到了最后一层的 \(\delta _ {L-1,j}\)。

我们递归的向后回溯,假设已知所有 \(\delta _ {l,j}\) ,如果求 \(\delta _ {l-1,j}\) 。

\[\vec{\delta} _ {l-1}= \triangledown _ {\vec{z} _ {l-1}}C\]

根据链式求导法则 \[ \begin{align} \vec{\delta} _ {l-1} &= \left(\matrix{W} _ {l-1}^T \vec{\delta} _ {l} \right)\otimes \sigma'(\vec{z} _ {l-1}) \end{align} \]

  • 其中 \(\vec{\delta} _ {l-1}\) 是 \(N _ {l-1}\) 维向量。
  • 其中 \(\vec{\delta} _ {l}\) 是 \(N _ {l}\) 维向量。
  • 其中 \(\matrix{W} _ {l-1}\) 是 \(N _ {l} \times N _ {l-1}\) 维矩阵。
  • 其中 \(\vec{z} _ {l-1}\) 是 \(N _ {l-1}\) 维向量。

成本函数与学习速度 sotfmax 函数与最大似然函数

本文参考 http://neuralnetworksanddeeplearning.com/chap3.html

softmax 函数可以替代 sigmoid 函数,同样,我们也需要修改成本函数,用最大似然函数替代平法和函数。

softmax 函数

\[ a _ j = \frac{e^{z _ j}}{\sum _ k e^{z _ {k}}} \]

这个函数有啥特点呢?就是每一级神经元的所有输出的和恒为 \(1\)

\[ \sum _ {j} a _ j = 1 \]

也就是说,类似一家公司,\(e^{z _ j}\) 就是每个神经元的出资,很有钱,随着输入 \(z _ j\) 成指数增长。 神经元的输出就是,这个神经元在本层中所占的股份多少。

类似书写识别,有的时候,我们更希望看到输出的结果是,有多少的概率可能是 0 , 有多少的概率是 1 ,诸如此类。 这个时候 softmax 函数看起来就很应景。

配合 softmax 函数,对应的成本函数就是最大似然函数

\[ C = - \ln a _ y \]

首先, \(C\) 恒大于零,因为 \(a _ y\) 在 \(0..1\) 之间。 \(C\) 表示对应输出如果是目标 y 的时候的似然概率。

如果 \(a _ y\) 等于 1 ,那么其他神经元的输出,对应 \(y _ j \neq y\) 的 \(a _ {y _ j} = 0\), 这是啥意思?

就是说,我们选取的权重和偏移量是多么的完美,导致其他神经元都识别说,是 \(y _ j\) 的概率为 \(0\) ,只有 对应的 \(a _ y\) 说,是 \(1\) 。

(TODO: 我自己有一个问题,如果不是识别 0-9 个手写字符,而是物品识别,就是说 \(y\) 的空间非常大,这个方法合适吗?)

经过一番推导,学习速度和 cross entropy 类似。

(TODO: 为啥选取这个东西,意义不明显)

(TODO: 问题:softmax 函数的输出依赖于其他神经元,BP 的推导过程也许又不一样的地方)

成本函数与学习速度

本文参考 http://neuralnetworksanddeeplearning.com/chap3.html

back propagation 里提到模型, 我们十分关心 \(\frac{\partial{C}}{\partial{w _ {l-1,j,i}}}\) 和 \(\frac{\partial{C}}{\partial{b _ {l,j}}}\) ,因为这个是学习速度,或者叫做训练速度。这个值很大的话,就会很快收敛。训练一个神经网路需要很大的计算量,如果能提高 \(\frac{\partial{C}}{\partial{w _ {l-1,j,i}}}\) 和 \(\frac{\partial{C}}{\partial{b _ {l,j}}}\),那么我们就可以很大提升训练效率。他们的含义是指 \({w _ {l-1,j,i}}\) 变化一点点,导致 \(C\) 变化是不是很大。我们希望越大越好。因为梯度求解过程中,我们知道 \(C\) 一定会变小,奔向最低点,那么何不让他跑的快一点。

成本函数

成本函数需要有几个特点

  1. 非负。 \(C>0\)
  2. 收敛性。当输出越接近目标值的时候,\(C\) 越小。当输出等于目标值的时候,\(C\) 为零。

一个简单的成本函数是通常平方和函数。

\(C = \frac{1}{2} \sum _ {k=0}^{N _ L - 1} (y _ {k}-a _ {L-1,k})^2\)

back propagation 中,我们看到下面的公式

\( \begin{align} \frac{\partial{C}}{\partial{w _ {l-1,j,i}}} &= \delta _ {l,j} a _ {l-1,i} \ \frac{\partial{C}}{\partial{b _ {l,j}}} &= \delta _ {l,j} \ \end{align} \)

其中

\( \begin{align} \delta _ {L-1,j} &= \frac{\partial{C}}{\partial{a _ {L-1,j}}} \sigma'({z _ {L-1,j}}) \ \delta _ {l-1,j} &= \sum _ {i=0}^{N _ {l} -1} \delta _ {l,i} w _ {l-1,j,i} \sigma'(z _ {l-1,j}) \end{align} \)

这里看到 这两个值主要的大小和 \(\frac{\partial{C}}{\partial{a _ {L-1,j}}}\) 和 \(\sigma'(x)\) 成正比例关系。

对于平方和函数来说

\( \frac{\partial{C}}{\partial{a _ {L-1,j}}} = (a _ {L-1,j} - y _ j) \)

当我们猜测了一个权重值和偏移量,导致神经元产生了很大的 \(z _ {L-1,j}\),如果选定 sigmoid 函数 \(\sigma(x)=\frac{1}{1+e^{-x}}\) 的时候

\(\sigma'(x) = \sigma(x) (1- \sigma(x))\)

可以 \(x\) 趋近于 \(+\infty\) 或者 \(-\infty\) 的时候, \(\sigma(x)\) 趋近于 \(0\) 或者 \(1\) 。

如果不改变神经元函数的前提下,也就是说,无论如何, \(\sigma'(x)\) 趋近于都 \(0\)。这个时候,学习速度十分缓慢。如何提高学习速度?

重复提一遍,学习速度是指 \(\frac{\partial{C}}{\partial{w _ {l-1,j,i}}}\) 和 \(\frac{\partial{C}}{\partial{b _ {l,j}}}\) 这两个值主要的大小和 \(\frac{\partial{C}}{\partial{a _ {L-1,j}}}\) 和 \(\sigma'(x)\) 成正比例关系。

既然 \(\sigma'(x)\) 已经很小了,接近于 \(0\) ,那么我们需要改变成本函数 \(C\),提高 \(\frac{\partial{C}}{\partial{a _ {L-1,j}}}\)

如果选择另一个 cross entropy 成本函数 (TODO: 下面的公式似乎不够清晰) \( C = - \frac{1}{n} \sum _ {x} (y\log a + (1-y) \log(1-a))) \)

这样 \( \begin{align} \frac{\partial{C}}{\partial{a _ {L-1,j}}} &= -\frac{1}{n} \left( y _ {j} \frac{1}{a _ {L-1,j}} + (1- y _ j) \frac{1}{1-a _ {L-1,j}} \cdot -1 \right) \ &= -\frac{1}{n} \left( \frac{y _ j}{a _ {L-1,j}} + \frac{y _ j - 1}{1-a _ {L-1,j}} \right) \ &= -\frac{1}{n} \frac{y _ j - y _ j a _ {L-1,j} + a _ {L-1,j} y _ j - a _ {L-1,j}} {a _ {L-1,j}(1-a _ {L-1,j})}\ &= \frac{1}{n} \frac{a _ {L-1,j} - y _ j } {a _ {L-1,j}(1-a _ {L-1,j})} \ \end{align} \)

(TODO 检查上面的推导,是不是最后 \({a _ {L-1,j}(1-a _ {L-1,j})}\) 可以约掉了?因为 \(\sigma'(x) = \sigma(x) (1- \sigma(x))\))

可以看到,这样的成本函数,在 \(a _ {L-1,j}\) 趋近于 \(1\) 或者 \(0\) 的时候, 都会导致 \(\frac{\partial{C}}{\partial{w _ {l-1,j,i}}}\) 变得很大。

cross entropy 函数

cross entropy 成本函数从哪里来的?我们从小的应试教育中,学会是什么,基本上考试没问题了, 能知道为什么,也就是推导过程,那么就可以很扎实的举一反三。但很少想这个东西是怎么来的,这就是创造力。

其实神仙也不能直接想到这个函数,实际上是倒着推出来的。首先我们假定需要偏微分的形式,希望成本函数对权重的偏导数和误差大小成正比关系,然后解偏微分方程,就可以求得这个函数。然后作者写论文的时候,有可能不需要把自己创造性的思维过程写出来,只写出结论。

cross entropy 的物理含义是什么呢? https://en.wikipedia.org/wiki/Cross _ entropy 里面有详细介绍。

文章开头的参考文章中说互熵主要描述我们的吃惊程度,我不是特别理解,后悔当初信息论没学好。

用 Makefile + pandoc + markdown 写博客

更新,已经移植到了 mdbook 工具了。看这里

静态页面生成有很多工具,我用过 Jekyll, Hugo, Hexo 等等。我原来用 Jekyll 写笔记,其实也够用了。最近想画图,这个东西不能自动集成 metapost ,也是挺麻烦的。

这些静态网页工具,功能太多,有很多其实不需要,这的需要自定义一些功能,折腾这些架构又过于复杂。例如

  • 集成 metapost , dot 等画图工具。我自己读过一个别人写的 hexo plugin 支持 dot 集成,但是折腾一次很麻烦。
  • template 模式过于复杂,每个工具都有自己的一套自定义模板语言
  • 不支持 asciidoc 等混合格式。

本来我想继续使用 Jekyll 的,但是突然间需要让我升级 ruby , 然后升级 gem 然后升级 bundle ,然后升级 jekyll ,然后说有一个依赖关系找不到。 WTF

我不是 ruby 的专家。我不想折腾了。

Hexo 类似的问题,升级完 nodejs ,一大堆问题。

于是,我试着用原始的方法生成网页。类似 unix 下的哲学,每个工具完成一点点工作,然后把他们组合在一起。于是,我用到下面这些工具。

  • atom + markdown preview plus 支持编辑和预览
  • Makefile 管理生产方式
  • pandoc 处理 markdown 生产 html
  • asciidoc , 处理 asciidoc ,生产 html
  • MathJax 处理公式
  • metapost 处理画图。
  • perl 生产 index.html

需要安装

%  brew install mactex
% brew install pandoc
% apm install markdown-preview-plus

Makefile 一般 unix 系统都自带的。

first_blog.html: first_blog.md
    pandoc -s --output $@ $<

然后 make first_blog.html 就可以生成了。这下自由了,不需要安装一个编程语言环境,就可以搞定网页生成了。 而且每个模块是可以替换的。如果觉得 pandoc 不好,可以用 kramdown 等等其他命令行工具处理 markdown 。

我的 Makefile 比这个复杂一些,也是抄别人的。 感兴趣的可以

% git clone https://github.com/wcy123/wcy123.github.io
% git checkout pandoc

这里面每段代码都很简单,类似浆糊弄在一起。

css 我找了 solarized light scheme 。直接 copy paste 过去的。

"backpropagation"

链式求导法则

单变量的复合函数

\( \begin{align} z &= z(y) \ y &= y(x) \ \frac{dz}{dx} &= \frac{dz}{dy} \frac{dy}{dx} \end{align} \)

多变量的复合函数

\( \begin{align} z &= f(u _ 1, u _ 2, ..., u _ n) \ u _ i &= u _ i(x) \ \frac{dz}{dx} &= \sum _ {i=1}^{n}\frac{dz}{du _ i}\frac{du _ i}{dx} \end{align} \)

神经网络建模

\( \begin{align} z _ {l,j} &= b _ {l,j} + \sum _ {i=0}^{N _ {l-1} -1 } w _ {l-1,j,i} a _ {l-1,i} \ a _ {l,j} &= \sigma(z _ {l,j}) \end{align} \)

其中

  • \(L\) 为神经网络的层数,包含输入层,包含输出层。上面的例子中是 4 层神经网络
  • \(l = 0,...,L-1\) 是层的下标
  • \(N _ {l}\) 是第 \(l\) 层神经网络的神经元的个数
  • \(j = 1, ..., N _ {l}\) 每一次的第 \(j\) 个神经元的下标
  • \(z _ {l,j}\) 是第 \(l\) 层神经网络中,第 \(j\) 个神经元的 sigmoid 函数的输入。
  • \(a _ {l,j}\) 是第 \(l\) 层神经网络中,第 \(j\) 个神经元的 sigmoid 函数的输出。特别的,当 \(l=0\) 时, \(a _ {0,i}\) 表示输入层,\(N _ 0\) 表示输入层的个数。
  • sigmoid 函数可以是,例如 \(\sigma(x) = \frac{1}{1+e^{-x}}\)
  • \(w _ {l-1,j,i}\) 是第 \(l-1\) 层中第 \(i\) 个输出和 \(l\) 层的第 \(j\) 个输入之间的权值。
  • \(b _ {l,i}\) 是第 \(l\) 层中第 \(j\) 个神经元的偏移量

back propagation

目的

给定一个训练样本 \(a _ {0,j}\), 和期待的输出 \(y _ {k}\), \(k=1...N _ {L-1}\) 。

\(a _ {0,j}\) 这个数学符号,\(0\) 表示第零层,即输入层。\(j =1...N _ {0}\) 表示第 \(0\) 层有 \(N _ {0}\) 个神经元

定义成本函数如下

\(C = \frac{1}{2} \sum _ {k=0}^{N _ L - 1} (y _ {k}-a _ {L-1,k})^2\)

那么,问题就是,如何选取一组 \(w _ {l,j,i}\) 和 \(b _ {l,i}\) ,使得 \(C\) 最小。

解决方案

梯度降低的方法,随机选取一组答案,然后沿着梯度方向,即 \(\frac{\partial{C}}{\partial{w _ {l-1,j,i}}}\) 和 \(\frac{\partial{C}}{\partial{b _ {l,j}}}\) ,减小。逐步找到合适的答案,\(w _ {l-1,j,i}\) 和 \(b _ {l,i}\) 。

那这里的问题就是,如何求 \(\frac{\partial{C}}{\partial{w _ {l-1,j,i}}}\) 和 \(\frac{\partial{C}}{\partial{b _ {l,j}}}\) ?

推导

如果定义

\(\delta _ {l,j}= \frac{\partial{C}}{\partial{z _ {l,j}}}\)

\(\delta _ {l,j}\) 的含义就是,如果神经元输入 \(z _ {l,j}\) 变化一点点,会导致成本函数 \(C\) 变化多少?

因为

\( z _ {l,j} = b _ {l,j} + \sum _ {i=0}^{N _ {l-1} -1 } w _ {l-1,j,i} a _ {l-1,i} \)

\(w _ {l-1,j,i}\) 变化一点点,会导致 \(z _ {l,j}\) 变化 \(a _ {l-1,j}\) 倍。

\(b _ {l,j}\) 变化一点点,会导致 \(z _ {l,j}\) 变化 同样的比例。

根据链式求导法则,就得出了下面的公式。

\( \begin{align} \frac{\partial{C}}{\partial{w _ {l-1,j,i}}} &= \frac{\partial{C}}{\partial{z _ {l,j}}} \frac{\partial{z _ {l,j}}}{\partial{w _ {l-1,j,i}}} \ &= \delta _ {l,j} a _ {l-1,i} \end{align} \)

类似的

\( \begin{align} \frac{\partial{C}}{\partial{b _ {l,j}}} &= \frac{\partial{C}}{\partial{z _ {l,j}}} \frac{\partial{z _ {l,j}}}{\partial{b _ {l,j}}} \ &= \delta _ {l,j} \end{align} \)

那么现在的关键是如何求 \(\delta _ {l,j}\)

思路类似于自然归纳法,就是先求 \(\delta _ {L-1,j}\) ,然后假设已知所有 \(\delta _ {l,j}\) ,如果求 \(\delta _ {l-1,j}\) 。

首先考虑第 \(L - 1\) 层,即最后一层。

\( \begin{align} C &= \frac{1}{2} \sum _ {k=0}^{N _ {L-1}-1} (y _ {k}-a _ {L-1,k})^2 \ \delta _ {L-1,j} &= \frac{\partial{C}}{\partial{z _ {L-1,j}}} \end{align} \)

根据链式求导法则

\( \begin{align} \delta _ {L-1,j} &= \frac{\partial{C}}{\partial{a _ {L-1,j}}} \frac{\partial{a _ {L-1,j}}}{\partial{z _ {L-1,j}}} \ &=(a _ {L-1,j} - y _ j) \sigma'(z _ {L-1,j}) \end{align} \)

这样,我们得到了最后一层的 \(\delta _ {L-1,j}\)。

我们递归的向后回溯,假设已知所有 \(\delta _ {l,j}\) ,如果求 \(\delta _ {l-1,j}\) 。

\(\delta _ {l-1,j}= \frac{\partial{C}}{\partial{z _ {l-1,j}}}\)

根据链式求导法则 \( \begin{align} \delta _ {l-1,j} &= \sum _ {i=0}^{N _ {l} -1} \frac{\partial{C}}{\partial{z _ {l,i}}} \frac{\partial{z _ {l,i}}}{\partial{a _ {l-1,j}}} \frac{\partial{a _ {l-1,j}}}{\partial{z _ {l-1,j}}} \ &= \sum _ {i=0}^{N _ {l} -1} \delta _ {l,i} \frac{\partial{z _ {l,i}}}{\partial{a _ {l-1,j}}} \frac{\partial{a _ {l-1,j}}}{\partial{z _ {l-1,j}}} \ &= \sum _ {i=0}^{N _ {l} -1} \delta _ {l,i} w _ {l-1,j,i} \sigma'(z _ {l-1,j}) \end{align} \)

上述公式的理解就是,第 \(l -1 \) 层的输入 \(z _ {l-1,j}\) 的变化,会导致成本 \(C\) 变化多少呢? 根据链式求导法则,这个 \(C\) 的变化量,会分散传导至第 \(l\) 层。第 \(l\) 层每个神经元分摊到的成本变化量是和 \(w _ {l-1,j,i}\) 成正比,也和 sigmoid 函数的变化率成正比。

理解 java 的 classloader

最近对 classloader 感兴趣, 于是研究了一下。下面是 classloader 的核心代码。

public Class<?> loadClass(String name) throws ClassNotFoundException;
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException;
protected Class<?> findClass(String name) throws ClassNotFoundException;
protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError;
protected final void resolveClass(Class<?> c);

可以看到,只有 loadClass(String) 是 public 的成语函数,其他几个函数都是 protected ,意味着是只有子类可以调用。

例如下面的 java 普通代码

class FactoryA {
    A build() {
          return new A();
    }
}

如果 A 是第一次被访问,也就是说在此之前,如果 class A 没有被加载过,那么隐含着下面的语句在其中。

  this.getClass().getClassLoader().loadClass("full.package.name.A");

https://en.wikipedia.org/wiki/Java_Classloader 和很多其他网上文章,都讲过一下 class loader 中的几个基础 class loader ,和他们的作用。

  • bootstrap class loader
  • Extensions class loader
  • System class loader

代理模式

大多数 class loader 会采用代理模式,因为默认的 ClassLoader 是这么做的。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
     // First, check if the class has already been loaded
     Class<?> c = findLoadedClass(name);
     if (c == null) {
         try {
             c = parent.loadClass(name, false);
         } catch (ClassNotFoundException e) {
             // ClassNotFoundException thrown if class not found
             // from the non-null parent class loader
         }
         if (c == null) {
             // If still not found, then invoke findClass in order
             // to find the class.
             c = findClass(name);
         }
     }
     if (resolve) {
         resolveClass(c);
     }
     return c;
}

这里是简化过的代码,去掉了关于锁,bootstrap 和统计的部分。可以看到,默认的实现是这样的。

  1. 首先判断是否已经加载过。如果加载过,用以前的,不会重复加载。
  2. 如果没有,那么代理给 parent 来加载,bootstrap class loader 没有 parent ,我们先忽略 bootstrap 相关的处理代码。
  3. 如果 parent 没有加载成功,那么调用 findClass 来处理。
  4. 调用 resolveClass 来连接(linking) 相关的依赖的类。这个也可能引入继续加载其他的类。

默认的 findClass 直接抛出 ClassNotFoundException

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

这意味着,如果我们自己写一个自定义的 class loader 类,那么我们最好重载 findClass 这个成员函数。

如果我们定义一个 findClass 那么我们如何返回一个 Class<?> 对象呢? 答案就是,调用 defineClass

首先,我们需要找到 class 的字节码,表示称为 byte[] ,然后传递给 defineClass 返回一个可用的 Class<?> 对象。

这里还有一个问题,this.getClass().getClassLoader() 返回的是哪一个 class loader 呢?

答案就是,如果某一个 class loader 调用了 defineClass 返回了某一个 Class<?> 对象,那么这个 Class<?>#getClassLoader() 函数,就会返回这个 class loader 。

这个规则看起来很简单,但是我们需要分析一下,可能的后果。

String 是一个很常用的类,是 bootstap class loader 定义的,换句话说,是 bootstrap class loader 调用了 defineClass 返回的 Class<String> 。那么,在 String 这个类里面,依赖的其他类,例如 Object ,就会直接调用 bootstrap class loader 来加载。不会使用 extension class loader 或者 system class loader 。

我们都想想这个规则,就会发现,如果子类的行为重新定义了搜索顺序,那么就很危险了,容易因为使用者的混乱。

例如 tomcat 的 war class loader ,他重新定义了 loadClass ,先看看自己能否找到 class ,如果找不到,然后再代理给 parent ,即 ear class loader 去搜索。

在 Java 中,我们尽量避免使用 null

null 在 java 里面是一个十分特殊的值,他可以是任何类型。我们应该尽量避免使用这个值,主要原因是当你使用任何 method 的时候,都会导致 NPE (NullPointerException)。

例如,

if(xxx.equals("hello")) {
    ...
}

这里 xxx 有可能是 null ,会导致 NPE ,所以有的时候,我们按照下面的方式写。

if("hello".equals(xxx)) {
    ...
}

但是如果比较 xxx, yyy 就麻烦了,

if(xxx.equals(yyy)) {
    ...
}

如果 xxx 是 null, 也会导致 NPE ,于是我们按照下面的方式写

if(xxx!=null && xxx.equals(yyy)){
    ...
}

但是,如果 yyy 也是 null ,这个判断结果是 false 。这个根据业务逻辑,某些情况下合理,某些情况下不合理。

甚至下面的代码都会可能产生 NPE

Boolean flag = getFlag();
if(flag) {
...
}

如果 flag 是 null ,就崩了。 Boolean 本来建模的是一个类型,类型中是一个集合,里面有两个元素,True 和 False ,可惜 Java 语言中,实际上是三值元素的集合,因为还有 null 这个万能值。这个严重影响了语义表达,影响建模。

如此基本的操作都会导致这么麻烦的代码和逻辑,那么,我们不如约定,所有的引用值,都不允许是 null 。某些注明的第三方库已经应用这种模式,例如 rxjava 2.0 。 rxjava 1.0 允许流中可以观察到 null 的值,而 rxjava 2.0 对此作出改进, 规定流中如果观察到了 null ,库直接抛 NPE 。 Guava 里面的某些类的设计,也是基于此原则。

如果有了这样一个约定,“所有引用值都不是 null ”,以上问题都解决了。也就是说,如果出现 NPE ,那么就以为这严重的逻辑 bug 。

有人想,如果我就想建模“可选”这个概念,如果某个操作成功,返回有效引用,如果失败,返回 null 。

有两个解决方案

  1. 改变设计,如果“操作”和 IO 相关,那么失败的时候,不是返回 null ,而是抛出自定异常。
  2. 如果“操作”和 IO 无关,不适合抛出自定义异常,那么使用 Optional 。

Java 8 里面提供了 java.util.Optional 这个类,显式建模这个“失败”的概念。

Integer parseInterger(String x){
   return Integer.valueOf(x)
}

上面的代码采用了第一种方案,如果转换整数失败,那么抛出 java.lang.NumberFormatException 的异常。

如果调用者不想处理异常,我们可以改成第二种方案。

Optional<Integer> parseInteger(String x) {
    try {
       return Optional.of(Integer.valueOf(x));
    }catch (NumberFormatException e){
       return Optional.empty();
    }
}

这个时候,调用者就需要

final Optional<Integer> aInt = parseInteger("123");
if(aInt.isPresent()){
    doSomething(aInt.get());
} else {
    doSomething(0); // assume default value is 0.
}

这么写显得很笨重,如果 Optional 多嵌套基层,代码就很难看了。

Optional<Double> aDouble;
if(aInt.isPresent()){
    aDouble = doSomethingWithInt(aInt.get());
}else {
    aDouble = doSomethingWithInt(0);
}
if(aDouble.isPresent()){
    doSomethingWithDouble(aDouble.get());
}else {
    doSomethingWithDouble(0.0);
}

我们可以改进一下

 Optional<Double> aDouble;
 Optional<Double> aDouble = doSomethingWithInt(aInt.orElse(0));
 doSomethingWithDouble(aDouble.orElse(0.0));

也就是说,orElse 提供了缺省值的模式。

如果说缺省值模式,不适合,例如,业务逻辑的需求是如果解析成功,做某事,否则什么都不做。

if(aInt.isPresent()){
    Optional<Double> aDouble = doSomethingWithInt(aInt.get(0));
    if(aDouble.isPresent()) {
        doSomethingWithDouble(aDouble.get());
    }
}

可以想象,每判断一次,程序向右缩进一次,代码也很难看,我们可以改进一下。

aInt.ifPresent(aIntegerValue -> {
    Optional<Double> aDouble = doSomethingWithInt(aIntegerValue);
    return aDouble.ifPresent(aDoubleValue ->{
        doSomethingWithDouble(aDoubleValue);
    });
});

我觉得这个很难称得上改进,但是 ifPresent 的确有应用的场景,我们换一个方式。

aInt.flatMap(YourClass::doSomethingWithInt)
    .flatMap(YourClass::doSomethingWithDouble)
    .orElse(aDefaultValue)

注: flatMap 就是 haskell 里面 bind 操作符, Optional 对应的就是 Maybe Monad 。

flatMap 很好的使用了这种模式。 map 有类似的操作。

Java 8 函数式编程例子

Java8 提供了函数式编程的能力,这里介绍过 java8 的高阶函数,这篇文章通过一个实际的例子,展示函数式编程的思维。

需求是这样的,有一个 log 文件,文件格式类似下面

2017/01/14 10:39:05 user "Peter" login
2017/01/14 11:49:05 user "John" login
2017/01/14 12:39:05 user "Peter" login
2017/01/14 12:39:12 user "Peter" login
2017/01/14 12:39:16 user "Emma" login
2017/01/14 12:39:17 user "Tom" login
2017/01/14 12:41:18 user "Emma" login
2017/01/14 12:42:25 user "Tom" login
2017/01/14 12:44:45 user "Peter" login
2017/01/14 12:45:55 user "Peter" login

输入是这样一个文件,希望输出一个统计结果,按照登录次数排序,如果登录次数相同,按照登录时间倒序排序。

类似

ExLogAnalyzerTest.LogEntry(timestamp=2017-01-14T04:45:55Z, user=Peter, count=5)
ExLogAnalyzerTest.LogEntry(timestamp=2017-01-14T04:42:25Z, user=Tom, count=2)
ExLogAnalyzerTest.LogEntry(timestamp=2017-01-14T04:41:18Z, user=Emma, count=2)
ExLogAnalyzerTest.LogEntry(timestamp=2017-01-14T03:49:05Z, user=John, count=1)

首先,我们需要从输入文件得到一个 stream ,每一个元素是输入文件中的一行文字。

Stream<String> stream = Files.lines(Paths.get(fileName))

然后,每一行转换成为一个 Pojo 。这里使用了 lombok ,类似

@Value
@Builder
static class LogEntry {
    Instant timestamp;
    String user;
    Integer count;
}

解析一行日志,找到我们感兴趣的登录日志。

private static final Pattern p = Pattern.compile(
           "^([0-9]{4}/[0-9]{2}/[0-9]{2} *[0-9]{2}:[0-9]{2}:[0-9]{2}) *.*\"([a-zA-Z]*)\".*login");
private static final SimpleDateFormat simpleDateFormat= new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
public static LogEntry parse(String line) {
       final Matcher matcher = p.matcher(line);
       if (matcher.find()) {
           try {
               final String dateString = matcher.group(1);
               return LogEntry.builder()
                       .timestamp(simpleDateFormat.parse(dateString).toInstant())
                       .user(matcher.group(2))
                       .count(1) // 登录次数默认为一次
                       .build();
           } catch (ParseException e) {
               e.printStackTrace();
               return null;
           }
       } else {
           return null;
       }
   }
}

这样我们就可以得到一组 log entry 了。

stream.map(LogEntry::parse)

log 文件中,可能有其他的 log,我们无法解析为 LogEntry ,那么就忽略这些记录

.filter(e -> e != null)

然后,我们要对分组,根据统计登录次数。

.collect(Collectors.groupingBy(LogEntry::getUser))

分组之后,返回的是一个 hashMap ,Key 是 String, 用户名, Value 是 List ,一组日志。list.size 就是登录次数。那么我们转换为登录次数和最后一次登录时间。

final Function<Map.Entry<String, List<LogEntry>>, List<LogEntry>> getValue
     = Map.Entry::getValue;
final Function<Map.Entry<String, List<LogEntry>>, LogEntry> mergeLoginEntry
     = getValue.andThen(list -> LogEntry.builder()
                    .count(loginCount.apply(list))
                    .timestamp(getLastLoginTime.apply(list))
                    .user(list.get(0).getUser())
                    .build());
.entrySet().stream().map(mergeLoginEntry)

对应的登录次数和最后一次登录时间的函数如下

final Function<List, Instant> getLastLoginTime =
    list ->
        Collections.max((List<LogEntry>) list, Comparator.comparing(LogEntry::getTimestamp))
             .getTimestamp();
final Function<List, Integer> loginCount = List::size;

然后按照登录次数排序,倒序,如果登录次数一样,就应该按照最后一次登录时间排序。

.sorted(Comparator.comparing(LogEntry::getCount)
                            .thenComparing(LogEntry::getTimestamp)

最后,我们输出结果

.collect(Collectors.toList())
.forEach(System.out::println);

结论,我们可以看到高阶函数很好的表达能力,高阶函数,就是指一个函数的参数或者返回值,还是一个函数。这个例子大量使用了高阶函数。

  • Comparator.comparing
  • Comparator::thenComparing
  • Collections::max
  • Stream::map
  • Stream::filter
  • Function::andThen
  • Stream::sorted
  • Collectors::groupingBy
  • List::forEach

延伸思考, Collector::groupingBy 需要创建一个全新的 list ,但是我们只需要统计 list 的长度和 list 中最大的登录时间,这里如果进一步优化,可以让 groupingBy 直接返回一个新的登录记录。用来减少空间复杂度,降低内存使用量。同时也可以减少后面排序的操作,降低了时间复杂度。

这里我们可以考虑使用 Collector::reduce ,函数式编程里面的万能妖刀。这个函数是很底层的函数,如果有其他函数能完成类似的功能,尽量不要使用这个函数。

在这个例子中,使用 Collector::reduce 可以让核心代码可以更加简洁了。

final BinaryOperator<LogEntry> reducer = (acc, a) -> LogEntry.builder()
         .count(acc.getCount() + 1)
         .timestamp(Collections.max(Arrays.asList(a.getTimestamp(), acc.getTimestamp())))
         .user(a.getUser())
         .build();
stream.map(LogEntry::parse)
         .filter(e -> e != null)
         .collect(Collectors.groupingBy(LogEntry::getUser, Collectors.reducing(reducer)))
         .entrySet().stream()
         .map(Map.Entry::getValue)
         .filter(Optional::isPresent)
         .map(Optional::get)
         .sorted(Comparator.comparing(LogEntry::getCount)
                 .reversed()
                 .thenComparing(LogEntry::getTimestamp)
                 .reversed())
         .collect(Collectors.toList())
         .forEach(System.out::println);

附录:完整的代码

package org.wcy123.fp.imp;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.Test;

import lombok.Builder;
import lombok.Value;

public class ExLogAnalyzerTest {
    @Test
    public void main() throws Exception {

        String fileName = "log.txt";

        try (Stream<String> stream = Files.lines(Paths.get(fileName))) {
            final Function<List, Instant> getLastLoginTime = list ->
                    Collections.max((List<LogEntry>) list, Comparator.comparing(LogEntry::getTimestamp)).getTimestamp();
            final Function<List, Integer> loginCount = List::size;
            final Function<Map.Entry<String, List<LogEntry>>, List<LogEntry>> getValue = Map.Entry::getValue;

            final Function<Map.Entry<String, List<LogEntry>>, LogEntry> mergeLoginEntry = getValue.andThen(list -> LogEntry.builder()
                    .count(loginCount.apply(list))
                    .timestamp(getLastLoginTime.apply(list))
                    .user(list.get(0).getUser())
                    .build());
            stream.map(LogEntry::parse)
                    .filter(e -> e != null)
                    .collect(Collectors.groupingBy(LogEntry::getUser))
                    .entrySet().stream()
                    .map(mergeLoginEntry)
                    .sorted(Comparator.comparing(LogEntry::getCount)
                            .reversed()
                            .thenComparing(LogEntry::getTimestamp)
                            .reversed())
                    .collect(Collectors.toList())
                    .forEach(System.out::println);


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Value
    @Builder
    static class LogEntry {
        private static final Pattern p = Pattern.compile(
                "^([0-9]{4}/[0-9]{2}/[0-9]{2} *[0-9]{2}:[0-9]{2}:[0-9]{2}) *.*\"([a-zA-Z]*)\".*login");
        private static final SimpleDateFormat simpleDateFormat =
                new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        Instant timestamp;
        String user;
        Integer count;

        public static LogEntry parse(String line) {
            final Matcher matcher = p.matcher(line);
            if (matcher.find()) {
                try {
                    final String dateString = matcher.group(1);
                    return LogEntry.builder()
                            .timestamp(simpleDateFormat.parse(dateString).toInstant())
                            .user(matcher.group(2))
                            .count(1) // 登陆次数默认为一次
                            .build();
                } catch (ParseException e) {
                    e.printStackTrace();
                    return null;
                }
            } else {
                return null;
            }
        }
    }
}

二分法查找

写一个二分法查找的程序,也没有那么容易。我试验了一下,写了一个。

int* search(int a[], int len, int v)
{
    if(len <= 0) {
        return NULL;
    }
    int mid = len/2;
    int c = a[mid];
    if(c == v){
        return a + mid;
    }else if(c > v) {
        return search(a, mid, v);
    }else {
        return search(a + mid + 1, len - mid - 1, v);
    }
    return NULL;
}

验证程序如下:

int main(int argc, char *argv[])
{
    int a[] = { 5,6,10,18,19};
    int len = sizeof(a) / sizeof(int);
    int * p = NULL;
    int b[] = {4,5,6,7,9,10,19,20};
    int lenb = sizeof(b)/sizeof(int);
    for(int i = 0; i < lenb; ++i){
        p = search(a,len, b[i]);
        if(p!=NULL){
            int d = p - a;
            printf("found %d at a[%d]\n", b[i], d);
        }else {
            printf("%d not found\n", b[i]);
        }
    }
    return 0;
}

程序输出结果如下:

4 not found
found 5 at a[0]
found 6 at a[1]
7 not found
9 not found
found 10 at a[2]
found 19 at a[4]
20 not found

写程序的时候,我脑子里需要记住几个基本规则,自己不能违反这些规则。

  1. 数组的长度是 len
  2. 数组最后一个元素是 a[len-1]
  3. len == 0 数组为空。
  4. 数组下标 a[mid],那么 a[mid] 之前有 mid 个元素。不包含 a[mid]
  5. 数组下标 a[mid],那么 a[mid] 后面一个元素的是 a[mid+1]
  6. 数组下标 a[mid],那么从 a[mid] 到数组结尾,一共有 len-mid 个元素。包含 a[mid]
  7. 特别的,当 mid = 0 的时候,从 a[0] 到数组结尾,一共有 len 个元素。包含 a[0]
  8. 如果数组下标 a[mid+1] ,那么从 a[mid+1] 到数组结尾,一共有 len - mid - 1 个元素。即,不包含 a[mid] 的时候,一共有多少个元素。

这个程序有一个很有争议的风格问题。

if(c==v) {
    return a + mid
}

是否可以在程序中间返回? 另一种风格是如下,

int* search(int a[], int len, int v)
{
    int * result = NULL;
    if(len <= 0) {
        result = NULL;
    }else {
        int mid = len/2;
        int c = a[mid];
        if(c == v){
            result = a + mid;
        }else if(c > v) {
            result = search(a, mid, v);
        }else {
            result = search(a + mid + 1, len - mid - 1, v);
        }
    }
    return result;
}

两种风格各有利弊,不做深聊了。

这个程序还有一个问题,就是尾递归。c 语言的尾递归是一种优化,不是语义上支持的。也就是说,当 len 非常非常大的时候,是否 stack overflow ,全凭 c 编译器优化开关的心情。如果我们不依赖于 c 的编译器的话,可以手工写尾递归的风格。

int* search(int a[], int len, int v)
{
    int * result = NULL;

TAIL_CALL:
    if(len <= 0) {
        result = NULL;
    }else {
        int mid = len/2;
        int c = a[mid];
        if(c == v){
            result = a + mid;
        }else if(c > v) {
            a = a;
            len = mid;
            v = v;
            goto TAIL_CALL;
        }else {
            a = a + mid + 1;
            len = len - mid - 1;
            v = v;
            goto TAIL_CALL;
        }
    }
    return result;
}

也就是说

result = search(<V1>, <V2>, <V3>);

被修改为:

a = <V1>
len = <V2>
c = <V3>
goto TAIL_CALL;

v=v 当然是没有必要的,这里为了强调是尾递归。

这种风格的尾递归,违反了大公司定义的风格。

  1. 不允许使用 goto
  2. 不允许修改过输入参数;

这里面还有一个风格的问题。就是如何写 if 语句。

  1. if 尽量带 else
  2. if elseif else 风格的代码,最好覆盖所有的可能性。

讨论详见 2015-07-08-C_C++-编程风格:-if-else.html

最重要的设计原则

https://www.youtube.com/watch?v=5tg1ONG18H8

Make interfaces easy to use correctly and hard to use incorrectly By Scott Meyers

随机笔记:

如果你设计一个接口,你的用户愿意成功的使用这个接口,也愿意对一点点文档,但是用户还是错误的使用了这个接口。这不是用户的错误,是设计者的错误。

用户在接触一个接口的时候,他会猜想这个接口干些啥事,他会猜想接口应该怎么样工作,但是当他们使用的时候,如果接口没有按照他猜想的工作,他会感到很吃惊。这不是用户的错,是设计者的错误。接口设计者的工作,就应该最大化的迎合用户的猜想。设计者应该充分利用日常常识,路径依赖的历史,多数人的直觉,约定俗成的模式,社区惯例,周围环境等等因素,最大程度的符合用户的猜想。

做到这一步的第一条准则就是 “选择一个好名字”。这很难,因为好名字都被别人先选走了。

“名字”就是“接口” !

当用户接触一个“接口”的时候,首先碰到的就是“名字”,于是,名字就显得特别重要。

C 语言写的快排程序

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

void quickSort(int a[], int len)
{
    if(len <= 0) {
        return;
    }
    int c = a[0];
    int mid = 0;
    for(int i = 1; i < len; ++i){
        if(a[i] < c){
            a[mid++] = a[i];
        }
    }
    a[mid] = c;
    quickSort(a, mid);
    quickSort(a + mid, len - mid - 1);
    return;
}

int main(int argc, char *argv[])
{
    int a[] = {5,3,1,2,4};
    int len = sizeof(a)/sizeof(int);
    quickSort(a, len);
    for(int i = 0; i < len; ++i){
        printf("a[%d] = %d\n", i,a[i]);
    }
    return 0;
}

首先找到第一个元素 c ,然后找到中点的位置,mid ,同时保证 a[0]a[mid-1] 都小于 ca[mid+1]a[len-1] 都大于 c 。之后把 c 放在,a[mid] 的位置。

最后,递归排序比 c 小的部分,和比 c 大的部分。

这个排序重用了原始数据的内存地址,但是递归调用的时候,需要用到额外的栈空间,所以空间复杂度不是 O(1)

编写单链表反转的程序

首先,我们定义一个链表。

struct list {
    int value;
    struct list * next;
};

C 语言也要面向对象编程,于是,编写构造函数。

struct list * makeList(int value, struct list * next){
    struct list * this = (struct list*)malloc(sizeof(struct list));
    this->value = value;
    this->next = next;
}

编写反转程序。

struct list * reverse(struct list * L) {
    struct list * result = NULL;
    for(struct list * i = L; i != NULL; i = i->next){
        result = makeList(i->value, result);
    }
    return result;
}

OK,让我们测试一下。

void printList(struct list * L)
{
    printf("(");
    int n = 0;
    for(struct list * i = L; i != NULL; i = i->next, ++n){
        if(n != 0){  printf(","); };
        printf("%d", i->value);
    }
    printf(")");
    return;
}
int main(int argc, char *argv[])
{
    struct list * a = makeList(1, NULL);
    a = makeList(2, a);
    a = makeList(3, a);
    printf("before reverse:");
    printList(a);
    printf("\nafter reverse:");
    printList(reverse(a));
    printf("\n");

    printf("before reverse:");
    printList(NULL);
    printf("\nafter reverse:");
    printList(reverse(NULL));
    printf("\n");
    return 0;
}

程序输出

before reverse:(3,2,1)
after reverse:(1,2,3)
before reverse:()
after reverse:()

这个程序的风格是 imperative 的,如果使用函数式编程,我们得换一个语言 Java 。

    public static class List<T> {
        private final T value;
        private final List<T> next;

        public List(T value, List next) {
            this.value = value;
            this.next = next;
        }

        @Override
        public String toString() {
            int n = 0;
            StringBuilder sb = new StringBuilder();
            sb.append("(");
            for(List i = this; i!=null; i = i.next, n ++){
                if(n!=0) {
                    sb.append(",");
                }
                sb.append(i.value);
            }
            sb.append(")");
            return sb.toString();
        }
    }

构造函数

    public static <T> List<T> cons(T value, List<T> next) {
        return new List(value, next);
    }

著名的 foldl 函数,一切一次遍历循环的抽象。

    public static <T,R> R foldl(BiFunction<T,R,R> f, R r, List<T> l){
        if(l == null){
            return r;
        }else{
            return foldl(f, f.apply(l.value, r), (List<T>)l.next);
        }
    }

可惜 java 不支持 TCO (Tail Call Optimization) ,这种写法会爆栈的。于是,用命令式的编程风格再写一次。

    public static <T,R> R foldl(BiFunction<T,R,R> f, R init, List<T> l){
        R result = init;
        for(List<T> i = l; i!=null; i = i.next){
            result = f.apply(i.value, result);
        }
        return result;
    }

反转列表的函数

    static <T> List<T> reverse(List<T> l) {
        return foldl(ListTest::cons, null, l);
    }

测试代码

    @Test
    public void main() throws Exception {
        List a = new List(1, null);
        a = new List(2, a);
        a = new List(3, a);

        System.out.println("before reverse: " + a);

        List b = reverse(a);
        System.out.println("after reverse: " + b);
    }

输出结果

before reverse: (3,2,1)
after reverse: (1,2,3)

foldl 可以抽象很多一次遍历的循环,例如,max 可以如下

    static <T extends Comparable<? super T>> T max(List<T> l){
        if(l == null) return null;
        return foldl((r1,r2) -> Comparator.<T>naturalOrder().compare(r1,r2) > 0?r1:r2,l.value, l);
    }

在函数式编程中,reverse-foldl 是一个俗语。例如 map 可以写成下面的形式。

    static <T,R> List<R> map(Function<T,R> f, List<T> l) {
        return reverse(foldl((r,n) -> cons(f.apply(r),n), null,l));
    }

测试代码

    @Test
    public void main2() throws Exception {
        List<Integer> a = new List(1, null);
        a = new List(2, a);
        a = new List(3, a);
        System.out.println("map: " + map(v-> v + 100, a));
    }

输出结果

map: (103,102,101)

Feign client

  1. @EnableDiscoveryClient
  2. pom 中包含
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
  1. 打开参数 spring.cloud.discovery.enabled=true。 参考 EnableDiscoveryClientImportSelector::isEnabled

  2. 打开 spring.cloud.consul.discovery.enabled=true 参考 ConsulDiscoveryProperties

  3. 打开 spring.cloud.consul.enabled=true 参考 ConsulProperties

  4. 记得加

headers = {"Accept = application/json", "Content-Type = application/json"}

否则返回值会无法解码

enable debug

@Bean
Logger.Level logLevel() {
    return Logger.Level.FULL;
}

官方参考手册 http://cloud.spring.io/spring-cloud-netflix/spring-cloud-netflix.html

logging.level.<YOURINTERFACE>=trace

如果看到下面的日志,可以调试 rest 输入输出。

2016-12-29 19:20:49.460 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] ---> GET http://CrmRestServer/v1/crm/tenants/1/agents/aa986a74-bf25-4f64-85be-e1fcf9726069/customers HTTP/1.1
2016-12-29 19:20:49.462 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] Accept : application/json
2016-12-29 19:20:49.462 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] Content-Type : application/json
2016-12-29 19:20:49.463 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] Content-Type: application/json;charset=UTF-8
2016-12-29 19:20:49.463 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] Content-Length: 94
2016-12-29 19:20:49.463 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal]
2016-12-29 19:20:49.463 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] {"page":"1","size":"20","userTagIds":"221312","visitorName":"abc","beginDate":"","endDate":""}
2016-12-29 19:20:49.464 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] ---> END HTTP (94-byte body)



2016-12-29 19:20:52.200 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] <--- HTTP/1.1 404 Not Found (2736ms)
2016-12-29 19:20:52.202 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] Transfer-Encoding: chunked
2016-12-29 19:20:52.202 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] Server: Apache-Coyote/1.1
2016-12-29 19:20:52.202 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] X-Application-Context: CrmRestServer:8590
2016-12-29 19:20:52.202 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] Date: Thu, 29 Dec 2016 11:20:52 GMT
2016-12-29 19:20:52.202 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] Content-Type: text/json;charset=UTF-8
2016-12-29 19:20:52.203 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal]
2016-12-29 19:20:52.360 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] {"timestamp":1483010451922,"status":404,"error":"Not Found","message":"No message available","path":"/v1/crm/tenants/1/agents/aa986a74-bf25-4f64-85be-e1fcf9726069/customers"}
2016-12-29 19:20:52.361 DEBUG 56282 --- [CrmRestServer-2] com.easemob.weichat.rest.mvc.dep.CrmApi  : [CrmApi#queryCustomerInternal] <--- END HTTP (174-byte body)
logging.level.com.netflix.loadbalancer=debug

例如,可以看到下面的东西,就是错的。很有可能 spring.cloud.consul.enabled=true 没有配置

2016-12-29 19:17:00.202 DEBUG 55945 --- [CrmRestServer-2] c.n.l.DynamicServerListLoadBalancer      : List of Servers for CrmRestServer obtained from Discovery client: []

如果看到下面这样,那么 consul 就是对的。

2016-12-29 19:20:51.015 DEBUG 56282 --- [erListUpdater-0] c.n.l.DynamicServerListLoadBalancer      : Filtered List of Servers for CrmRestServer obtained from Discovery client: [172.17.1.201:8590]

设置超时阈值

hystri‌x.command.default.ex‌ecution.isolation.th‌read.timeoutInMillis‌econds=5000

这个只供调试用。因为把默认的超时都改成了 5s

hystri‌x.command.<methodName>.ex‌ecution.isolation.th‌read.timeoutInMillis‌econds=30000

设置参数使Fallback可用

feign.hystrix.enabled=true

关于协议设计语言

通信系统中存在无数个协议,协议就是把事先规定好的一些规则,发送端按照规则发送,接收端按照规则理解。这个规则,某种程度上来说,就是一门语言。接收端和发送端都符合一个协议,就像他们都是讲同一门语言。很难想象,HTTP 的客户端可以连接 IMAP 服务器。

相比编程语言,协议语言更加简单,注重如何抽象的描述信息,而不在话信息如何在内存中存储和表示。这种描述,通常叫做抽象语法表示(ASN1)。互联网中也出现了一些其他的类似的定义,例如 Protobuf 。

这样的一门语言通常就是数据结构的描述。一般分为简单的数据类型和复杂数据类型。

后向兼容与平滑演进

无数协议设计证明,我们无法一次就把协议设计好,协议永远会有下一个版本。版本的后向兼容和平滑演进变得十分重要。

兼容需要考虑两个问题。

  • 发送者是新版本,接收者是旧版本
  • 发送者是旧版本,接收者是旧版本。

后面每种数据类型,都应该考虑这个问题。

简单数据类型

  • 整数
  • 浮点数
  • 枚举体

整数,例如 1,2,3 。

整数一般包含一些属性,描述整数类型本身。例如,范围,即最大值,最小值。理论上这个必须设置,否则理论上,发送者可以发送一个无限大的整数,无论底层如何编码,发送的信息量无穷大,无论带宽多大都传输不过去。

最简单的定义范围的方法就是定义类似 int8, uint8, int16, uint16, int32, uint32, int64, uint64 等等的类型。

对于后向兼容来说,新版本可以让整数的范围变大,但是不能让范围变小。

  • 旧版本发送者发送给新版本的接收者,因为范围不会超,所以不会有问题。
  • 新版本发送者发送给旧版本的接收者,如果超过范围,接收者则无法接收。导致不兼容。

浮点数,一般很少用到。通信中,也常常用定点数代替浮点数。小数也就是一种特殊的整数。

枚举体,例如 "红“, ”黑“, ”白“。这个应用最广。其实和整数类似。但是比整数更加准确的描述语义。

简单的多线程并不能提高效率

下面是一个最简单的多线程处理程序。这种多线程处理并不能提高效率

private ExecutorService threadPool = Executors.newFixedThreadPool(2);

//..
Output process(Input input) {
    Future<Output> f =
        threadPool.submit(() ->
           longTimeConsumingTask(input));
  return f.get();
}

这里似乎看到 longTimeConsumingTask 在其他线程里面并行计算,但可惜的是 f.get() 在这里同步等待。

这种方式其实和在本线程里面运行 longTimeConsumingTask 区别不大。

JAVA8 中的高阶函数

A Few Hidden Treasures in Java 8 on YouTube 是一个很有意思的视频。

函数式编程有一个很大的特点就是高阶函数。在很多”函数式“ 语言中,“函数”都是”第一公民“,就是说,函数可以像整数,浮点数,字符串等等普通值一样,可以作为函数的参数,也可以作为成员变量,函数的返回值。

在 JAVA 中,函数当然不是第一公民,函数是只有一个虚成员函数的接口。

如果一个函数的的参数是函数,或者返回值是函数,那么这个函数就是高阶函数。

任何非静态成员函数都可以看成一个函数,第一个参数是 this

下面是一个例子

public class SortPersonTest {
    public static List<Person> createPeople() {
        return Arrays.asList(
                new Person("Sara", Person.Gender.FEMALE, 20),
                new Person("Sara", Person.Gender.FEMALE, 22),
                new Person("Bob", Person.Gender.MALE, 20),
                new Person("Paula", Person.Gender.FEMALE, 32),
                new Person("Paul", Person.Gender.MALE, 32),
                new Person("JACk", Person.Gender.MALE, 2),
                new Person("JACK", Person.Gender.MALE, 72),
                new Person("Jill", Person.Gender.FEMALE, 12));
    }

    public static void main(String[] args) {

        createPeople().stream().sorted(
                Comparator.comparing(Person::getGender)
                        .reversed()
                        .thenComparing(Person::getAge)
                        .thenComparing(Person::getName))
                .forEach(System.out::println);
    }
}

程序输出

Person(name=Jill, gender=FEMALE, age=12)
Person(name=Sara, gender=FEMALE, age=20)
Person(name=Sara, gender=FEMALE, age=22)
Person(name=Paula, gender=FEMALE, age=32)
Person(name=JACk, gender=MALE, age=2)
Person(name=Bob, gender=MALE, age=20)
Person(name=Paul, gender=MALE, age=32)
Person(name=JACK, gender=MALE, age=72)

sorted 是一个高阶函数,第一个参数是 Stream this 。第二个参数是一个 Comparator 接口,这个接口只有一个虚函数,所以可以认为把一个函数作为参数。

Comparator::comparing 也是一个高阶函数,第一个参数是一个函数(Function),返回值也是一个函数(Comparator)。 类似实现可以是

    interface MyComparator<T> extends Comparator<T> {};
    public static <T, U extends Comparable<? super U>>
    MyComparator<T> comparing( Function<? super T, ? extends U> fKey) {
        return (t1, t2) -> fKey.apply(t1).compareTo(fKey.apply(t2));
    }

请忽略 Generic 的部分,这个不是本文的重点。

reversed 同样也是一个高阶函数,第一个函数是 Comparator (this) ,返回值还是一个函数 (Comparator),reversed 的实现可以类似下面

        default MyComparator<T> myReversed(){
            return (t1,t2) -> this.compare(t2,t1);
        }

thenComparing 的实现类似下面的代码

         default <U extends Comparable<? super U>> MyComparator<T> myThenComparing(Function<? super T, ? extends U> fKey) {
            return (t1, t2) -> {
                final int r1 = compare(t1, t2);
                return r1 == 0 ? myComparing(fKey).compare(t1,t2): r1;
            };
        }

完整代码

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;

public class SortPersonTest {
    public static List<Person> createPeople() {
        return Arrays.asList(
                new Person("Sara", Person.Gender.FEMALE, 20),
                new Person("Sara", Person.Gender.FEMALE, 22),
                new Person("Bob", Person.Gender.MALE, 20),
                new Person("Paula", Person.Gender.FEMALE, 32),
                new Person("Paul", Person.Gender.MALE, 32),
                new Person("JACk", Person.Gender.MALE, 2),
                new Person("JACK", Person.Gender.MALE, 72),
                new Person("Jill", Person.Gender.FEMALE, 12));
    }
    interface MyComparator<T> extends Comparator<T> {
        default MyComparator<T> myReversed(){
            return (t1,t2) -> this.compare(t2,t1);
        }
        default <U extends Comparable<? super U>> MyComparator<T> myThenComparing(Function<? super T, ? extends U> fKey) {
            return (t1, t2) -> {
                final int r1 = compare(t1, t2);
                return r1 == 0 ? myComparing(fKey).compare(t1,t2): r1;
            };
        }
    };

    public static <T, U extends Comparable<? super U>>
    MyComparator<T> myComparing(Function<? super T, ? extends U> fKey) {
        return (t1, t2) -> fKey.apply(t1).compareTo(fKey.apply(t2));
    }
    public static void main(String[] args) {

        createPeople().stream().sorted(
                myComparing(Person::getGender)
                        .myReversed()
                        .myThenComparing(Person::getAge)
                        .myThenComparing(Person::getName))
                .forEach(System.out::println);
    }
}

JAVA JSON databinding 的多态

JSON 在 REST API 的调用中越来越多的应用,如何表达多态是经常碰到的一个问题。

有三种方式解决这个问题

  • PROPERTYEXISTING_PROPERTY, POJO 映射为 JSON Object ,某一个字段表示类型。
  • WRAPPER_OBJECT POJO 映射为只有一个字段的 JSON Object ,字段名表示类型。
  • WRAPPER_ARRAY POJO 映射为两个元素的数组 ,前面的表示类型,后面的表示指值。

背景

@Data
static class  Zoo {
    private Animal leader;
    private Animal[] followers;
}

假设有一个 Zoo 类,里面包含了一个特殊的动物 leader,和一些动物 followers。下面的代码试图转换 JSON

public static void main(String[] args) throws IOException {
    Zoo zoo = new Zoo();
    zoo.setLeader(new Dog());
    zoo.setFollowers(new Animal[]{new Dog(), new Cat()});
    final ObjectMapper mapper = new ObjectMapper();
    final String json = mapper.writeValueAsString(zoo);
    System.out.println(json);
    final Zoo zoo2 = mapper.readValue(json, Zoo.class);
    System.out.println(zoo2);
}

例子 DogCat

static class Dog implements Animal {
    private String name = "wangwang";
    @Override
    public String getAnimalType() {
        return "dog";
    }

    @Override
    public String getName() {
        return name;
    }
    @Override
    public void setName(String name) {
        this.name = name;
    }
}
static class Cat implements Animal {
    private String name = "miao";

    @Override
    public String getAnimalType() {
        return "cat";
    }

    @Override
    public String getName() {
        return name;
    }
    @Override
    public void setName(String name) {
        this.name = name;
    }
}

把 POJO 映射称为 JSON Object , include = JsonTypeInfo.As.EXISTING_PROPERTY

include = JsonTypeInfo.As.PROPERTY 也类似,区别不大。

用字段 animalType 来表示类型信息。

{
  "leader": {
    "name": "wangwang",
    "animalType": "dog"
  },
  "followers": [
    {
      "name": "wangwang",
      "animalType": "dog"
    },
    {
      "name": "miao",
      "animalType": "cat"
    }
  ]
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY,
        property = "animalType")
@JsonSubTypes({
        @JsonSubTypes.Type(value = Dog.class, name = "dog"),
        @JsonSubTypes.Type(value = Cat.class, name = "cat")})
@JsonIgnoreProperties(ignoreUnknown = true)
public interface Animal {
    String getAnimalType();
    String getName();
    void setName(String name);
}

这种类型还有很多变种。例如 use 可以选择 JsonTypeInfo.Id.CLASS 之类的。

把 POJO 映射称为数组 , include = JsonTypeInfo.As.WRAPPER_ARRAY

{
  "leader": [
    "dog",
    {
      "name": "wangwang",
      "animalType": "dog"
    }
  ],
  "followers": [
    [
      "dog",
      {
        "name": "wangwang",
        "animalType": "dog"
      }
    ],
    [
      "cat",
      {
        "name": "miao",
        "animalType": "cat"
      }
    ]
  ]
}

每个POJO 映射为一个两个元素的 JSON 数组,第一个元素表示类型,第二个元素表示真正的内容。

把 POJO 映射称为包装对象 nclude = JsonTypeInfo.As.WRAPPER_OBJECT

{
  "leader": {
    "dog": {
      "name": "wangwang",
      "animalType": "dog"
    }
  },
  "followers": [
    {
      "dog": {
        "name": "wangwang",
        "animalType": "dog"
      }
    },
    {
      "cat": {
        "name": "miao",
        "animalType": "cat"
      }
    }
  ]
}
{
  "leader": {
    "dog": {
      "name": "wangwang",
      "animalType": "dog"
    }
  },
  "followers": [
    {
      "dog": {
        "name": "wangwang",
        "animalType": "dog"
      }
    },
    {
      "cat": {
        "name": "miao",
        "animalType": "cat"
      }
    }
  ]
}

每个 POJO 映射为一个 只有一个 property 的 POJO Object ,这个 property name 表示类型信息。 property value 是 POJO 真正的内容。

rxjava2 有什么新东西

昨天看过Jakes Whaton 的一个视频 ,今天记录一下我脑子里还记得东西。也许不全,以后在仔细补全吧。

backpressure 怎么翻译,我翻译成流控吧。

rxjava1.x 里面最难的部分就是流控,我有一个文章专门分析了源代码,设计的不是很好。实现的也不是很好。

流控在 rxjava1.x 很晚期才引入的,所以一开始,并没有很好的设计。rxjava1.x 早期没有注意到流控的问题,后期发现有这个问题,于是引入了一些修改,而且这些修改很好的和原有的代码一起工作,就发版了。然而,流控是很难的东西,没有一个很好的设计,就会导致很多问题,不是所有的 source 和 operator 都意识到有流控这个东西,经常出现 MissingBackpressureException 。 就算 rxjava 1.x 自带的一些 operator 和 source 有的时候也会产生这个异常。rxjava1.x 里面的 Obervable.create 就不应该被任何人使用。这个 API 十分有用,而且十分强大,但是没有任何明确的说明,如何和流控一起好好的工作。

rxjava2.0 重新设计了接口。增加了对流控的重新设计。源代码层面上,完全重写了。

rxjava2.0 引入了 Flowable 这个类,基本上和 rxjava 1.0 的 Obserable 一样, 支持流控。

rxjava2.0 中的 Observable 类,则是完全不支持流控。这样大家可以随意使用 create 函数了。没有了流控,代码变得简单清晰,干净多了。

rxjava2.0 在类型系统上,支持显示的流控设计。Flowable 和 Obserable 不能互相混用,意思是说,源和操作符不能混用。Flowable 有自己的一套操作符和源,几乎和 Obserable 一模一样,但 Flowable 的操作符,不能用在 Observable 上,反之亦然。这样的好处就是,在代码的编译器,在类型检查阶段,就避免了流控的滥用。如果你有意识的使用流控,那么就用 Flowable ,否则就用 Obserable。有流控的操作符和没有流控的操作符,完全分开了。

Flowable 和 Observable 之间支持互相转换。这样我们在需要流控的地方,小心的使用 Flowable ,需要的时候,经过转换, Obserable 的世界,变得清楚多了。反之亦然,也可以把 Obserable 转换成为 Flowable 。转换的时候,我们需要显式的指明流控方式。

Rxjava2.0 里面不允许有 null 的数据。 null 是 java 的一个令人头疼的数据,任何对象,都有可能是 null ,这样违反了原来数据类型的语义。例如 Boolean 对象,本来要表示两种状态 , true 和 false ,但是,其实他有三种状态,true, false, null 。所以代码逻辑上就很让人纠结。于是 Rxjava2.0 完全不允许 null 出现在数据流中。我想这是一个不小的改进。

但是如果我们需要表示有些对象是无效值怎么办?例如,我就是想表示某一个 Boolean 字段不是 true, 也不是 false,而是表示没有这个字段,网络协议中,经常发送者者为了节省带宽,有些可选字段就不发送。数据库里面,有些字段就是没有填写。rxjava 2.0 引入了 Maybe 这个类型,用来建模这种情况。

类似的,还有新类型 Single ,在类型系统上,保证语义上,一个产生一个单值。还有新类型 Completable ,在类型系统上,保证不会产生任何数据,仅仅产生 OnComplete 或者 OnError 。这种编译器的类型检查,防止代码错误出现在运行期,是一个很大的改进。

还与其他的一些改进,我记不住了,今天就写到这里吧。

阅读 Subscriber 的实现中关于 backpressure 的部分

rxjava 中最具有挑战性的设计就是 backpresure 。例如 zip 操作符,合并两个 Observable A 和 B 。如果 B 的产生速度比 A 快,那么就需要不停的缓存 B 多余生成出来的数据,这样内存就无限增长了。 backpressure 的机制就是让 B 生成慢一点。

目前为止,我看到 rxjava 的设计是很丑陋的。这种机制是没有强制性的。更糟糕的是, rxjava 暴露了 Observable.create(OnSubscribe<?> onSubscribe) 这个函数,如果不了解这个机制,上来"想当然" 的实现一个 OnSubscribe ,而不管 backpressure 机制,很容易产生 MissingBackpressureException

“想当然” 不是使用者的错,而是库的设计者的错误。可惜的是,太多用户重度使用这个 Observable.create(OnSubscribe<?> onSubscribe) 函数,为了保证现有程序能够继续运行,就不能隐藏这个函数。于是,我们在注释中,可以看到下面一段话

This method requires advanced knowledge about building operators and data sources; please consider other standard methods first;

本文试图得到 “advanced knowledge” 。

下面是 backpressure 的协议是如何建立的。

someObservable.subscribe(new Subscriber<T>() {
    @Override
    public void onStart() {
      request(1);
    }

    @Override
    public void onCompleted() {
      // gracefully handle sequence-complete
    }

    @Override
    public void onError(Throwable e) {
      // gracefully handle error
    }

    @Override
    public void onNext(T n) {
      // do something with the emitted item "n"
      // request another item:
      request(1);
    }
});

可见底层 subscriber 在刚刚启动的时候,发起流控请求 onStart , request(1) 。告诉楼上的,哥们,别整太多,一个数据就够了,多了处理不了。 onNext 中,先处理数据,处理完了,告诉楼上的,接着往下放数据,别多,就一个。

这里需要注意的是,不能再 request(n) 函数里面产生数据,否则递归调用 onNext ,可能导致爆栈了。

我们看看 Subscriber 是如何实现这个协议的。

public abstract class Subscriber<T> implements Observer<T>, Subscription {
// represents requested not set yet
private static final long NOT_SET = Long.MIN_VALUE;
private final SubscriptionList subscriptions;
private final Subscriber<?> subscriber;
private Producer producer;
private long requested = NOT_SET; // default to not set
}

本文重点关注 backpressure ,只看和这个相关的变量

  • NOT_SET 表示无效的请求数据量。或者说,还 Subscriber 没有提供请求的数据量时的状态。
  • subscriber ,如果这个值不为 null,那么把 backpressure 相关的处理,交给这个 subscriber 处理。有大多数很多操作符,自己并不能很好的处理这种过载,需要一层层向上传递,一直到数据源,只有产生数据的地方,才能比较好的处理,因为在那里,可以很容易的少产生一些数据。
  • producer 如果本 subscriber 可以处理,那么代理给 producer 来处理。
  • requested ,计数器,记录楼下的请求多少数据。
    • 如果是 NOT_SET ,就是说楼下还不知道请求多少。
    • 如果是 MAX_LONG ,就是说楼下来者不拒,不怕 overload
    • 如果是其他值,就是说楼下的最多能处理多少数据。
 protected final void request(long n) {
       // if producer is set then we will request from it
       // otherwise we increase the requested count by n
       if (producer != null) {
            producer.request(n);
       } else {
            requested = requested + n;
       }
}

这个函数被我简化了,去掉了关于线程安全的部分。这样代码的可读性好多了。

  • 就是说如果有 producer ,那么计数的功能就交给 producer 了。
  • 如果没有,那么 requested 用来计数。

这里简化了代码,去掉了 requested 溢出的处理,就是说当 requested + nLONG_MAX 还要大的时候,会防止其变成负数。

public void setProducer(Producer p) {
    boolean passToSubscriber = subscriber != null && requested == NOT_SET;
    producer = p;
    if (passToSubscriber) {
        subscriber.setProducer(producer);
    } else {
        if (requested == NOT_SET) {
            producer.request(Long.MAX_VALUE);
        } else {
            producer.request(requested);
        }
    }
}

同样,这里去掉了关于线程安全的代码。

个人认为,setProducer 这个函数名字起的不好,因为这个函数除了设置 producer 成员变量之外,还会调用 produce.request 函数。

再来分析一下这个 setProducer 函数

  • 底层是否掉用过本层的 request(n)
    • 如果调用过,requested != NOT_SET,意味着底层出发了流控请求。
    • 如果没有调用过,requested == NOT_SET,意味着底层没有出发了流控请求。
  • producer 是真正处理流控的逻辑。subscriber 把流控逻辑交给 producer处理。如果没有 producer , subscriber 也就只能简单的计数,根本处理不了流控。
  • 如果在触发流控请求之前,setProducer 函数被调用,那么要看本层是否愿意处理这个流控请求。
    • 如果成员变量 subscriber 不是空,那么表示本层 Subscriber 不愿意,或者不能够处理好这个 backpressure ,交个上层处理 subscriber.setProducer(producer)
    • 上层如果不产生数据,本层的 OnNext 也不会触发。从而达到了流控的目的。这样一层一层往上传,一直要交给数据源那一层才好处理。换句话说,如果你需要创建了一个 Observable,例如你写了一个新的 operator ,但是不能很好地处理 backpressure ,那么最好往上传递。在 OnSubscribe 的时候,把本层 subscriber和上层 subscriber 串起来。
    • 如果本层愿意处理 backpressure 请求,那么就调用 procuder.request 处理请求。
  • 如果是在触发流控请求之后, setProducer 被调用,那么无论本层是否愿意,都要处理这个请求。

代码虽短,这个逻辑太复杂了。

小结

这里刚刚是一个皮毛,真正的 producer 处理流控逻辑还没有提到。下次有时间,专门分析一个真正的流控逻辑。

同时,我们也看到,最好不要自己写 operator 和 OnSubscribe ,而是调用现成的 from 系列函数, createSync 之类的提供流控的工厂方法,构造 Observable。

阅读 rxjava 源代码之 - map

上一篇文章 写了一个极其简化的 Rxjava Observable ,现在,我试图添加一个 map 操作符。

public <R> Observable<R> map(Func1<T, R> func1) {
  return new Observable<R>(subscriber -> this.subscribe(new Subscriber<T>() {
      @Override
      public void onCompleted() {
          subscriber.onCompleted();
      }

      @Override
      public void onError(Throwable e) {
          subscriber.onError(e);
      }

      @Override
       public void onNext(T t) {
           R r = null;
           try {
               r = func1.call(t);
           } catch (Throwable e) {
               unsubscribe();
               return;
           }
           subscriber.onNext(r);
       }

      @Override
      public void unsubscribe() {
          subscriber.unsubscribe();
      }

      @Override
      public boolean isUnsubscribed() {
          return subscriber.isUnsubscribed();
      }
  }));
}

Java 本身语言限制,导致代码臃肿。代码的核心部分就是

return new Observable<R>(subscriber -> this.subscribe(new Subscriber<T>() {
    @Override
     public void onNext(T t) {
           R r = null;
           try {
               r = func1.call(t);
           } catch (Throwable e) {
               unsubscribe();
               return;
           }
           subscriber.onNext(r);
       }
}

这里看到以下几点

  1. map 接收两个参数 ,注,对于成员函数,第一个参数是 this
  2. 第一个参数是 Observable<T> this
  3. 第二个参数是 Func1<T,R> func1;
    1. func1 接收一个参数 T
    2. func1.call(t) 返回一个 R
  4. map 要返回一个 Observable<R> ,那么就要在 OnSubscribe 的时候,需要从 this 里面得到一个个 T t ,然后用 func1.call(t) ,然后转移给下一个 subscriber

因为 Java8 lambda 关键字的引入,我们看到函数式编程中的 variable capture 的强大。

这是一个非常简化的 map 实现,还有很多问题。

  1. 还有非常多的操作符和 map 很类似,这里有很多重复代码。
  2. backpressure 没有处理。
  3. unsubscribe 还没有处理好。subscriber 链的关系没有处理。
  4. 异常也没有处理好。
  5. 没有保证 onComplete 只被调用一次 。

这个简化的实现尽管有很多问题,但是可以帮助我们理解原有复杂完整的实现。Map 的核心结构是这样

  1. 本身含有一个 Subscriber 对象,订阅上层的 Observable
  2. 返回一个 Observable 对象,提供给下层订阅。
  3. 这种方法组合了 Observable,构成了一个链条。
OnSubscribe<?> onSubscribe = subscriber /*传递进来的 subscriber 参数,给下层产生数据*/
     -> {
        /* this 是上层的 Observable,订阅上层 */
        this.subscribe(new Subscriber<T>() {
        @Override
        public void onNext(T t) {
           R r = null;
           try {
               r = func1.call(t);
           } catch (Throwable e) {
               unsubscribe();
               return;
           }
          /* 当上层产生数据的时候,经过转换,传递给下层*/
           subscriber.onNext(r);
       }
      };
Observable<R> ret = new Observable<R>(onSubscribe);
return ret;
}

rxjava 如何和传统回调函数结合

今天看到一个 Observable.fromEmitter 的函数,这里是这个函数的 javadoc

Provides an API (via a cold Observable) that bridges the reactive world with the callback-style, generally non-backpressured world. Example: You should call the AsyncEmitter's onNext, onError and onCompleted methods in a serialized fashion. The rest of its methods are thread-safe.

Observable.<Event>fromEmitter(emitter -> {
Callback listener = new Callback() {
@Override
public void onEvent(Event e) {
emitter.onNext(e);
if (e.isLast()) {
emitter.onCompleted();
}
}

@Override
public void onFailure(Exception e) {
emitter.onError(e);
}
};

AutoCloseable c = api.someMethod(listener);

emitter.setCancellation(c::close);

}, BackpressureMode.BUFFER);

这是一个实验性的功能,用于和传统回调模式的程序对接。为了理解这个机制,我写了一个完整的例子。

package org.wcy123.rxjava1;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

import org.junit.Test;

import lombok.extern.slf4j.Slf4j;
import rx.AsyncEmitter;
import rx.Observable;

@Slf4j
public class FromEmitterTest {

    @Test
    public void main1() throws Exception {
        final ExecutorService service = Executors.newCachedThreadPool();
        final CountDownLatch latch = new CountDownLatch(3 * 4);
        Observable.fromEmitter(
                 emitter -> IntStream.range(0, 3).boxed().forEach(
                        threadIndex -> service.submit(
                                () -> {
                                    for (int i = 0; i < 4; ++i) {
                                        emitter.onNext("thread + " + threadIndex
                                                + " i = " + i);
                                        Utils.sleep(1000);
                                        latch.countDown();
                                    }
                                    if (threadIndex == 2) {
                                        emitter.onCompleted();
                                    }
                                })),
                AsyncEmitter.BackpressureMode.BUFFER)
                .subscribe(s -> log.info("item {}", s));
        log.info("提前打印这里, subscribe 没有阻塞住");
        log.info("开始等待解锁");
        latch.await();
        log.info("解锁完毕");
    }
}

这个例子的执行结果是

02:12:24.244 [main] INFO org.wcy123.rxjava1.FromEmitterTest - 提前打印这里, subscribe 没有阻塞住
02:12:24.244 [pool-1-thread-1] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 0 i = 0
02:12:24.250 [main] INFO org.wcy123.rxjava1.FromEmitterTest - 开始等待解锁
02:12:24.251 [pool-1-thread-1] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 1 i = 0
02:12:24.251 [pool-1-thread-1] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 2 i = 0
02:12:25.245 [pool-1-thread-2] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 2 i = 1
02:12:25.255 [pool-1-thread-1] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 0 i = 1
02:12:26.248 [pool-1-thread-3] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 2 i = 2
02:12:26.249 [pool-1-thread-3] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 1 i = 2
02:12:26.257 [pool-1-thread-1] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 0 i = 2
02:12:27.252 [pool-1-thread-2] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 2 i = 3
02:12:27.258 [pool-1-thread-1] INFO org.wcy123.rxjava1.FromEmitterTest - item thread + 0 i = 3
02:12:28.264 [main] INFO org.wcy123.rxjava1.FromEmitterTest - 解锁完毕

注意到 log.info("item") 是运行在三个不同的线程中。fromEmitter 的第一个参数是一个函数,即 f, 该函数的第一个参数是 emitter ,类型是 AsyncEmitter。fromEmitter 返回一个 Observable , 这个Obverable 被订阅的时候,就会运行函数 f 。f 运行时,创建了 3 个线程,每个线程里面,都会调用 emitter 来发布数据,emitter.onNext(...) ,一旦调用这个函数,会触发后面所有的 Observable 定义的行为,触发 s->log.info("iterm{}", s) 的执行。

注意到 thread 0 并没有机会打印出来最后一个 i = 3, 因为 thread 2 提前调用了 emitter.onComplet()

latch.await() 等待所有线程结束。

阅读 rxjava 源代码

介绍

事件驱动异步调用是两种慢慢被大家接收的编程范式了。rxjava 库利用观察者模式,把事件驱动异步调用程序组合在一起。

基于异步调用和事件驱动的程序,经常陷入回调陷阱,导致程序的可读性下降,写出来的程序像意大利面条(callback hell, callback spaghetti)。参考 http://callbackhell.com。

本文从源代码级别上,介绍了 rxjava 中最重要的几个接口和类。

Obserable

final Observable<String> observable = Observable.empty();

Observable 只有一个成员变量 onSubscribe

onSubscribe 只有一个成员方法

Observer

class SimpleObserver implements Observer<String> {
     @Override
     public void onCompleted() {}
     @Override
     public void onError(Throwable e) {}
     @Override
     public void onNext(String s) {}
}
final Observer<String> observer = new SimpleObserver();

observer 没有任何成员变量。

subscribe

observable.subscribe(observer);

这一堆 subcribe 函数都落在了 Subscriber 参数的那个版本上了,返回值都是 Subscription

image::images/observable-11bb0.png[]

subscriber 其实就是 observer ,也是 subscription。

subscription 用来停止订阅 unsubscribe

subscribe 比较矫情的一段代码,简化如下。

class Observable<T> {
  public final Subscription subscribe(Subscriber<? super T> subscriber) {
      onSubscribe.call(subscriber);
      return subscriber;
  }
}
  1. subscriber 就是传进去的第一个参数 observable.subscribe(observer)subscriber 也是一个 Observer, 因为 Subcriber 继承 Observer
  2. 这个参数 subscriber 传递给对象 onSubscribecall 方法, onSubscribe.call(subscriber)
  3. subscriber 作为一个 Subscription 返回。 因为 Subscriber 继承 Subscription

由此可见 Subscriber 这个类才是 rxjava 的核心。subscriber 对象不停的在各个类之间流转。 各种各样不同 OnSubscribe 接口的实现,可以去产生数据源,然后调用 subscriber.onNext(data)

小结

一个超级简化版的 rxjava 可以是下面这个样子。

public interface OnSubscribe<T> extends Action1<Subscriber<? super T>> {
}
public class Observable<T> {
    final OnSubscribe<T> onSubscribe;

    public Observable(OnSubscribe<T> onSubscribe) {
        this.onSubscribe = onSubscribe;
    }
    public final Subscription subscribe(Subscriber<? super T> subscriber) {
        onSubscribe.call(subscriber);
        return subscriber;
    }

}
public interface Observer<T> {
    void onCompleted();
    void onError(Throwable e);
    void onNext(T t);
}
public interface Subscription {
    void unsubscribe();
    boolean isUnsubscribed();
}
public abstract class Subscriber<T> implements Observer<T>, Subscription {
}

尽管 Subscriber 类所占的篇幅很小,他确实核心的一个类。

测试一下这段代码

    @Test
    public void main() throws Exception {
        final Observable<String> observable = new Observable<>(subscriber -> {
            subscriber.onNext("hello world");
            subscriber.onCompleted();
        });
        final Subscriber<String> subscriber = new Subscriber<String>() {
            @Override
            public void onCompleted() {
                System.out.println("byebye");
            }
            @Override
            public void onError(Throwable e) {
            }
            @Override
            public void onNext(String s) {
                System.out.println("> " + s);
            }
            @Override
            public void unsubscribe() {
            }
            @Override
            public boolean isUnsubscribed() {
                return false;
            }
        };
        observable.subscribe(subscriber);
    }
    ```

使用 Protobuf 设计 REST API

概述

一个设计的好的 REST API 接口,需要一个严格的接口定义。本文试图使用 Protobuf 作为接口设计语言,设计 API。

创建文件,main/proto/Login.proto

syntax = "proto3";
package org.wcy123.api;
option java_outer_classname = "Protos";
message LoginRequest {
    string name = 1;
    string password = 2;
}
message LoginResponse {
    enum LoginResult {
        DEFAULT = 0;
        OK = 1;
        FAIL = 2;
    }
    LoginResult result  = 1;
}

然后创建一个 Bean 用于转换 JSON 到 Java 对象

@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
  return new ProtobufHttpMessageConverter();
}

然后创建 Controller

@RestController
@Slf4j
public class MyRestController {
    @RequestMapping(path = "/login", method = POST)
    public ResponseEntity<Protos.LoginResponse> login(HttpServletRequest request,
            HttpServletResponse response, @RequestBody Protos.LoginRequest command) {
        log.info("input is {}", new JsonFormat().printToString(command));
        return ResponseEntity.ok().body(Protos.LoginResponse.newBuilder()
                .setResult(Protos.LoginResponse.LoginResult.OK)
                .build());
    }
}

Protos.LoginRequestProtos.LoginResponse 是自动生成的

测试程序

curl -v -s -H 'Content-Type: application/json' -H 'Accept: application/json' http://127.0.0.1:8080/login -d '{"name":"wcy123", "password":"123456"}'
*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /login HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.43.0
> Content-Type: application/json
> Accept: application/json
> Content-Length: 39
>
* upload completely sent off: 39 out of 39 bytes
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Tue, 22 Nov 2016 10:03:43 GMT
<
* Connection #0 to host 127.0.0.1 left intact
{"result": "OK"}

从外部看开,请求和响应都是 JSON,PB 只是内部实现。注意要加 Content-Type:application/jsonAccept: application/json 两个 header。

Protobuf 支持的常用数据类型和 JSON之间的转换关系。

syntax = "proto3";
package org.wcy123.api;
message Root {
    Obj1 obj1 = 1;
    bytes base64 = 2;
    enum AnEnum { FOO_BAR = 0 ; GOOD = 2; BYE = 3;};
    AnEnum anEnum = 3;
    repeated string anArray = 4;
    bool aBoolean = 5;
    string aString = 6;
    int32 aInt32 = 7;
    uint32 aUint32 = 8;
    fixed32 aFixed = 9;
    int64 aInt64 = 10;
    uint64 aUint64 = 11;
    fixed64 aFixed64 = 12;
    float aFloat = 13;
    double aDouble = 14;
    //Timestamp aTimestamp = 15;
    //Duration aDuaration = 16;
    oneof anOneOf {
        string eitherString = 17;
        int32 orAnInt = 18;
    }
}
message Obj1{
    string name = 1;
}

我写了一个例子,用于实验所有的改动,具体文档,参考 https://developers.google.com/protocol-buffers/docs/proto3#json 这个转换成 JSON 是下面这个样子。

{
  "obj1": {
    "name": "YourName"
  },
  "base64": "abc123!?$*&()'-=@~",
  "aBoolean": true,
  "aString": "hello world",
  "aInt32": -32,
  "aUint32": 32,
  "aFixed": 64,
  "aInt64": -64,
  "aUint64": 64,
  "aFixed64": 128,
  "aFloat": 1,
  "aDouble": 2,
  "eitherString": "Now It is a String, not an integer"
}

注意到 oneOf 使用字段名称来表示哪一个字段被选择了。

细节

依赖关系

<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-java</artifactId>
  <version>3.0.0-beta-1</version>
</dependency>

需要插件

<plugin>
  <groupId>org.xolstice.maven.plugins</groupId>
  <artifactId>protobuf-maven-plugin</artifactId>
  <version>0.5.0</version>
  <configuration>
    <protocExecutable>/usr/local/bin/protoc</protocExecutable>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>compile</goal>
        <goal>test-compile</goal>
      </goals>
    </execution>
  </executions>
</plugin>

默认值的处理

注意到在 OKFAIL 前面,我放了一个无用的 DEFAULT 值。如果没有这个值,那么 JSON 转换的时候,认为 result 是默认值,那么就不会包含这个字段。

  • 如果要强制包含这个字段,那么填一个无用的默认值,占位,永远不用这个默认值。
  • 如果想节省带宽,默认值就不传输的话,那么就保留这个行为。接收端需要判断如果字段不存在,就使用默认值。

不能识别的 JSON 字段

PB 忽略不能识别的字段。

rabbitmq 中的概念

本文主要是阅读 https://www.rabbitmq.com/tutorials/amqp-concepts.html 之后的笔记

什么是消息 Message

  1. Headers , message meta data, message attributes
  2. Properties
  3. Payload

Headers 和 Properties 都是 KV(Key-Value) 对。Headers 给 AMQP 系统用的,Properties 给应用程序用的。 Payload 就是一大堆字节。

AMQP 中的三种 Entity(实体)

  • Queue (队列) 是一个 FIFO 缓存区。
  • Exchange (交换中心)是一个无状态的路由算法。
  • Binding (绑定),是 Exchange 和 Queue 之间的关系,控制 Exchange 如何转发消息到各个 Queue 。

Exchange

有4中 Exchange 的类型

  • Direct Exchange (直接转发)
  • Fanout Exchange (广播转发)
  • Topic Exchange (多播转发)
  • Header Exchange (根据包头转发)

默认 Exchange

这是一种特殊的 Exchange,类型是 Direct 。

每一个 Queue 在创建的时候,就都和默认 Exchange 之间有这个转发规则,即 binding 。所以,如果我们用 queue 的名字作为 routing key 发送一个消息给默认 Exchange ,看起来就像直接发送消息到 queue 里面一样。

Direct Exchange

消息到达 Exchange 之后,检查消息头里面的 routing key , 然后把消息转发给与 routing key 同名的 queue 。负载均衡是保证在所有 Consumers 负载相同,而不是 queue 的负载相同。

Fanout Exchange

routing key 没有用了。到达 Exchange 的消息,被转发给所有绑定的 Queue 。广播。

Topic Exchange

根据 routing key 来多播。只针对满足匹配条件的 queue ,消息才会被转发。

Header Exchange

不看 routing key ,而是看 Header 里面其他的属性。

可以有多个匹配条件。"x-match" 是一个 binding 的参数。如果 "x-match=any" ,那么任意条件满足,就认为匹配。如果 "x-match=all" ,那么必须所有条件都满足,才认为匹配。

Queue

和其他队列的概念类似

Bindings

Bindings 是一组规则。消息到达 Exchange 之后,根据这组规则,决定把消息转发到哪些 Queue 里面。

Consumers

消费者读取队列。有两种方式,“推” 和 “拉”。

“推”, 消费者先告诉 AMQP 服务器,他対哪些队列感兴趣;然后如果有消息到达队列,AMQP 把消息推送给消费者。同一个 Queue 可以注册多个消费者,也可以注册一个排他的消费者。

"拉“ 的方式是消费者自己主动询问队列里面是否有消息,有消息就发下来。

消息确认

消费者收到消息之后,消息并没有删除,需要消费者发送 ACK ,消息才真正删除。有两种模式

  • 隐式 ACK ,消费者一旦收到消息,立即自动回复 ACK。
  • 显式 ACK ,消费者收到消息之后,需要应用程序主动调用回送 ACK 的指令。

隐式 ACK 有一个风险,如果消费者处理消息的过程中,意外中断,那么消息就丢了。

显式 ACK 也有一个风险,如果这个消息消费者处理不了,无法回送 ACK ,那么会导致死循环,服务器会不停的发送这个消息到队列中。

拒绝消息

为了避免显式 ACK 的问题,应用程序可以告诉服务器,这个消息处理不了。ACK 可以同时通知服务多个消息应处理了。但是拒绝消息不行,不能告诉服务器说我一次拒绝了多个消息,必须一个一个消息的拒绝,这样效率低。

消息反向确认

为了避免拒绝消息的低效,有一种协议扩展,叫做 Negative ACK ,可以同时拒绝多个消息。

预获取消息

为了提高效率,消费者可以告诉服务,一下子给个我多个消息。支持 Channel 级别的 prefetch,不支持连接级别的 prefetch 。

连接

指 TCP 连接。可以是加密过的。

Channel

为每一个队列建立一个 TCP 连接的话,过于低效。Channel 是共享连接的方式。每一个协议包里面,都带有一个 Channel Id ,那么就可以在同一个 TCP 连接上,虚拟的建立多个 Channel ,Channel 之间完全隔离的。

从零开始构造一个微服务

创建一个 spring boot 的程序

目录结构如下

wangchunye@wangchunye hello-micro-service$ tree
.
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── wcy123
    │   │       └── org
    │   │           └── hello
    │   │               ├── HelloServerStarter.java
    │   │               └── SayHello.java
    │   └── resources
    └── test
        └── java

pom.xml 的内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!--
    名称三元组, groupId:artifactId:version
    -->
    <groupId>org.wcy123</groupId>
    <artifactId>hello-micro-service</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>a demo project for micro service</name>
    <!-- 表示输出是生成一个 jar 包 -->
    <packaging>jar</packaging>
    <properties>
        <!-- 标准的几个配置,否则编译的时候警告 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <!-- 使用 java8  -->
        <java.version>1.8</java.version>
    </properties>
    <!-- 使用 sprint boot  -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.3.RELEASE</version>
    </parent>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Brixton.SR6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <!-- 包含依赖关系,一个最简答的 rest service  -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

基于返回值的 java generic 类型推导

public class  HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }

    boolean foo() {
        return new Adapter().handle();
    }
    public static class Adapter {
        public <T> T handle(){
            return null;
        }
    }
}

这里遇到几个问题. 在类型推到的过程中, T 到底是什么类型, 我没有找到好的方法, 于是用看字节码的方法看.

javap -s -c ./HelloWorld.class
Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
    descriptor: ()V
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String hello world
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  boolean foo();
    descriptor: ()Z
    Code:
       0: new           #5                  // class HelloWorld$Adapter
       3: dup
       4: aload_0
       5: invokespecial #6                  // Method HelloWorld$Adapter."<init>":(LHelloWorld;)V
       8: invokevirtual #7                  // Method HelloWorld$Adapter.handle:()Ljava/lang/Object;
      11: checkcast     #8                  // class java/lang/Boolean
      14: invokevirtual #9                  // Method java/lang/Boolean.booleanValue:()Z
      17: ireturn
}
  • 8: 表示 T 经过 Type Erase , 实际都是返回 Object
  • 11: 表示, 在 foo 中, 推导出来的类型是 Boolean
  • 14: 表示在做 unbox , 这里会产生 NullPointerException

使用 Spring Integration Framework 写入 redis 队列

Spring Integration Framework 提供了应用集成的一种方式。各个应用程 序,组件,通过 Channel, Message 松散耦合在一起。

这个例子演示了如何从 stdin 读取用户输入,然后写入 redis 队列的。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.wcy123.example.spring.integration.redis</groupId>
	<artifactId>si-redis</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>si-redis</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.4.0.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-integration</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-stream</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-redis</artifactId>
        </dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

上面大部分是自动生成的,自己加的只有

        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-stream</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-redis</artifactId>
        </dependency>

引入 spring-integration-stream ,我们可以解析 xml 中的 namespace

xmlns:int-stream="http://www.springframework.org/schema/integration/stream"

spring-integration-stream 的引入,我们可以解析 xml 中的 namespace

xmlns:int-redis="http://www.springframework.org/schema/integration/redis">

下面的 spring boot 生成的程序框架。

// src/main/java/com/wcy123/example/spring/integration/redis/SiRedisApplication.java
package com.wcy123.example.spring.integration.redis;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
import org.springframework.integration.config.EnableIntegration;

@SpringBootApplication
@EnableIntegration
@ImportResource("classpath:/META-INF/spring/si-components.xml")
public class SiRedisApplication {

	public static void main(String[] args) {
		SpringApplication.run(SiRedisApplication.class, args);
	}
}

@EnableIntegration 标注一个 Configuration 类是 Spring Integration 的配置。

@ImportResource("classpath:/META-INF/spring/si-components.xml")

用来引入 spring integration 的 beam 定义。目前 4.3.0 中 ,不是所有的 spring integration 组件都支持 java annotation 的配置,很多还是依赖 xml 的配置。

src/main/resources/META-INF/spring/si-components.xml 的内容

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:int="http://www.springframework.org/schema/integration"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:int-stream="http://www.springframework.org/schema/integration/stream"
       xsi:schemaLocation="
	http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd
	http://www.springframework.org/schema/integration/stream http://www.springframework.org/schema/integration/stream/spring-integration-stream.xsd
	http://www.springframework.org/schema/integration/redis  http://www.springframework.org/schema/integration/redis/spring-integration-redis.xsd"
       xmlns:int-redis="http://www.springframework.org/schema/integration/redis">

    <int-stream:stdin-channel-adapter id="producer" channel="messageChannel"/>

    <int:poller id="defaultPoller" default="true"
                max-messages-per-poll="5" fixed-rate="200"/>

    <int:channel id="messageChannel"/>
    <bean id="redisConnectionFactory"
          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    </bean>
    <int-redis:queue-outbound-channel-adapter
            id="queue"
            channel="messageChannel" queue="a_queue"
            connection-factory="redisConnectionFactory"
            left-push="false"
    ></int-redis:queue-outbound-channel-adapter>
</beans>
    <int-stream:stdin-channel-adapter id="producer" channel="messageChannel"/>

    <int:poller id="defaultPoller" default="true"
                max-messages-per-poll="5" fixed-rate="200"/>

创建了一个 endpoint ,每隔 200 ms ,从标准输入读取数据,然后扔到 messageChannel 中。

<int:channel id="messageChannel"/>

创建了一个 messageChannel 的 channel。

    <int-redis:queue-outbound-channel-adapter
            id="queue"
            channel="messageChannel" queue="a_queue"
            connection-factory="redisConnectionFactory"
            left-push="false"
    ></int-redis:queue-outbound-channel-adapter>

会从 messageChannel 中读取 Message , 然后用 rpush 命令,把消息压到 消息队列 a_queue 中。

queue-outbound-channel-adapter 需要一个 redis connection factory

    <bean id="redisConnectionFactory"
          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" />

这里创建了一个 redis connection factory , 这个 bean 有很多属性,用于 表明连接的主机名称,密码,端口号,是否使用连接池等等。

发自己的语

本文大多数参考 SICP

阴阳大法

ying-yan

一个语言的解释器,就是一个 eval 函数,就是求值函数:

  • 输入: 一个表达式 s-exp
  • 输出: 一个表达式 s-exp

这个解释器的核心就是 applyeval 之间的互递归调用。

原始类型

原始类型是一个 s-exp ,求值就是 s-exp 本身。包括:整数,浮点数,字符串。例如

1  => 1
1.2 => 1.2
"abc" => "abc"

这个规则看上去很简单,但是很重要,他是递归求值的终止条件。也就是说, eval 函数碰到这些原始类型的 s-exp ,不会递归调用 apply

根据这个规则,我们写一个函数

(define true #t)
(define false #f)
(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        (else (error "Unknown expression type -- EVAL" exp))))

(define (self-evaluating? exp)
  (cond ((number? exp) true)
        ((string? exp) true)
        (else false)))

我们运行一下这个函数。首先我们需要安装一个 scheme 解释器,参考 https://github.com/cisco/ChezScheme 语言参考 http://www.scheme.com/tspl4/

Chez Scheme Version 9.4
Copyright 1984-2016 Cisco Systems, Inc.

> (load "eval0.scm")
> (eval 1 '())
1
> (eval 1.2 '())
1.2
> (eval "abc" '())
"abc"
> (eval (list 1 2 3) '())
Exception in error: invalid message argument (1 2 3)
Type (debug) to enter the debugger.

恭喜你,一个解释器的雏形开始了。这里有一个开发原则,“永远有一个可以工 作的版本”。我经历过两种开发模式。

  1. 写了一天的代码,从来没有编译运行过。然后花三天去调试。
  2. 写几分钟的代码,然后立即调试。循环往复,每次增加功能一点点。

第一种模式下,大多数情况是,我自己思路不很清晰,模块还没有分解到足够细 致,所以一边写代码,一遍划分模块。

第二种模式下,我已经思路很清晰了,代码模块也很清晰了,尤其是模块划分的 颗粒度足够细致,脑子里面已经有具体的迭代步骤了。这种模式下,开发效率高 一些。

可以看到,(eval (list 1 2 3) '() 的时候,出现错误。这里也有一个开 发原则,“测试你的每一行代码”。可以看到,这几个简单的测试,覆盖了刚刚写 的所有代码。

开发初期,保证代码的测试覆盖率相对容易。这个时候,最好配合自动回归测试, 保证以后代码的覆盖率。如果开发初期没有把自动化测试做好,到了开后中后期, 在回过头来想保证代码的测试覆盖率,难度就十分大了。

好了,我们继续迭代开发我们的解释器。

quote

这个规则也是一个终止条件。如果一个 s-exp 是一个 list ,第一个元素 是 quote,第二个元素任何一个 s-exp X,那么求值结果就是 X 。

(quote 1) => 1
(quote 1.2) => 1.2
(quote "abc") => "abc
(quote (1 2 3)) => (1 2 3)
(quote a-symbol) => a-symbol
(quote quote) => quote

s-exp 中有一个重要的数据类型,就是 symbol 。 quote 可以得到 symbol 本身。

(define (eval exp env)
  (cond (......
        ((quoted? exp) (text-of-quotation exp))
         ......
        (else (error "Unknown expression type -- EVAL" exp))))

我们看看 quoted?(text-of-quotation) 怎么实现的?

(define (quoted? exp)
  (tagged-list? exp 'quote))
(define (text-of-quotation exp) (cadr exp))
(define (tagged-list? exp tag)
  (if (pair? exp)
      (eq? (car exp) tag)
      false))

看看运行结果

> (load "eval1.scm")
> (eval '(quote 1) '())
1
> (eval '(quote 1.2) '())
1.2
> (eval '(quote "hello") '())
"hello"
> (eval '(quote a-symbol) '())
a-symbol
> (eval '(quote quote) '())
quote

eval1.scm 的完整内容如下

(define true #t)
(define false #f)
(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ((quoted? exp) (text-of-quotation exp))
        (else (error "Unknown expression type -- EVAL" exp))))

(define (self-evaluating? exp)
  (cond ((number? exp) true)
        ((string? exp) true)
        (else false)))

(define (quoted? exp)
  (tagged-list? exp 'quote))
(define (text-of-quotation exp) (cadr exp))
(define (tagged-list? exp tag)
  (if (pair? exp)
      (eq? (car exp) tag)
      false))

一个好的程序风格是,多写一些小的函数,然后组合这些函数。函数都有名字, 可以描述自己的作用。例如 text-of-quotation 。其实这个函数本质上和 cadr 没有任何区别。但是在逻辑层次上,text-of-quotation 是更加高层 的函数,cadr 是更加底层的函数。这种逻辑层次的划分,可以提高程序的可 读性。

if

支持条件表达式,输入 s-exp 是一个 list ,有四个元素:

  1. 第一个元素是 if ,表明是一个条件表达式。
  2. 第二个元素是判断条件 <C>
  3. 第三个元素是 <T> 为真的时候,s-exp 的求值结果。
  4. 第三个元素是 <F> 为假的时候,s-exp 的求值结果。

这里有一个语言设计的问题,是否有必要增加一个 boolean 的数据类型?什么 是真,什么是假?这里有很多让人难以捉摸的小细节。这个问题也是各种语言之 间争论很久的话题。为了简单,我们不在这个问题上展开讨论,我们不增加新的 数据类型,认为符号(symbol) true 是真,其他值都是假。

(define (eval exp env)
  (cond (......
        ((if? exp) (eval-if exp env))
         ......
        (else (error "Unknown expression type -- EVAL" exp))))

先看看 if? 的实现。

(define (if? exp) (tagged-list? exp 'if))

感谢逻辑分层的代码风格,if? 的实现的可读性就很好了。

我们看看关键的 eval-if 怎么实现的。

(define (eval-if exp env)
  (if (true? (eval (if-predicate exp) env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))

这里有两点需要注意

  1. 递归调用, eval-if 递归调用了 eval ,可以看到递归调用的强大之处。
  2. 求值顺序。

“求值顺序” 是一门语言的一个重大的设计问题。不同的求值顺序,导致完全不同 的效果。这里我们注意到,如果求值条件为真,那么假的分支是不做求值的。如 果求值条件为假,那么真的分支是不会求职的。这种设计叫做短路求值 (short-circuit)。 主流语言都是这么设计的。求值顺序是一个很重要的课题, 这里不展开讨论。

看看其他辅助函数如何实现的。这里再次应用了逻辑分层的代码风格。

(define (if-predicate exp) (cadr exp))
(define (if-consequent exp) (caddr exp))
(define (if-alternative exp)
  (if (not (null? (cdddr exp)))
      (cadddr exp)
      'false))
(define (true? exp)
  (eq? exp 'true))

我们看看程序的执行效果

> (eval '(if (quote true) 1 2) '())
1
> (eval '(if (quote false) 1 2) '())
2

非常好。 这里是完整的代码

(define true #t)
(define false #f)
(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ((quoted? exp) (text-of-quotation exp))
        ((if? exp) (eval-if exp env))
        (else (error "Unknown expression type -- EVAL" exp))))

(define (self-evaluating? exp)
  (cond ((number? exp) true)
        ((string? exp) true)
        (else false)))
(define (if? exp) (tagged-list? exp 'if))

(define (eval-if exp env)
  (if (true? (eval (if-predicate exp) env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))

(define (if-predicate exp) (cadr exp))
(define (if-consequent exp) (caddr exp))
(define (if-alternative exp)
  (if (not (null? (cdddr exp)))
      (cadddr exp)
      'false))
(define (true? exp)
  (eq? exp 'true))

(define (quoted? exp)
  (tagged-list? exp 'quote))
(define (text-of-quotation exp) (cadr exp))
(define (tagged-list? exp tag)
  (if (pair? exp)
      (eq? (car exp) tag)
      false))

代码块求值

这个功能是一个很方便的功能。如果 <s-exp> 是下面的形式

(begin <E1> <E2> .... <En>)

那么我们对 E1 , E2 ,.... , En 分别求值,整个表达式的值就是 En 的求值结果。

(define (eval exp env)
  (cond (.......
        ((begin? exp)
         (eval-sequence (begin-actions exp) env))
        ......

看看关键函数 eval-sequence 的实现

(define (eval-sequence exps env)
  (cond ((last-exp? exps) (eval (first-exp exps) env))
        (else (eval (first-exp exps) env)
              (eval-sequence (rest-exps exps) env))))

这里注意

  1. 求值顺序
  2. 递归调用 eval
  3. 中间的表达式的求值结果被丢弃了

看看其他几个辅助函数的实现

(define (begin? exp) (tagged-list? exp 'begin))
(define (begin-actions exp) (cdr exp))
(define (last-exp? seq) (null? (cdr seq)))
(define (first-exp seq) (car seq))
(define (rest-exps seq) (cdr seq))

看看程序的执行效果

> (load "eval3.scm")
> (eval '(begin 1 2) '())

下面是完整的程序代码

(define true #t)
(define false #f)
(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ((quoted? exp) (text-of-quotation exp))
        ((if? exp) (eval-if exp env))
        ((begin? exp)
         (eval-sequence (begin-actions exp) env))
        (else (error "Unknown expression type -- EVAL" exp))))

(define (self-evaluating? exp)
  (cond ((number? exp) true)
        ((string? exp) true)
        (else false)))
(define (if? exp) (tagged-list? exp 'if))

(define (eval-if exp env)
  (if (true? (eval (if-predicate exp) env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))

(define (if-predicate exp) (cadr exp))
(define (if-consequent exp) (caddr exp))
(define (if-alternative exp)
  (if (not (null? (cdddr exp)))
      (cadddr exp)
      'false))
(define (true? exp)
  (eq? exp 'true))

(define (eval-sequence exps env)
  (cond ((last-exp? exps) (eval (first-exp exps) env))
        (else (eval (first-exp exps) env)
              (eval-sequence (rest-exps exps) env))))

(define (begin? exp) (tagged-list? exp 'begin))
(define (begin-actions exp) (cdr exp))
(define (last-exp? seq) (null? (cdr seq)))
(define (first-exp seq) (car seq))
(define (rest-exps seq) (cdr seq))

(define (quoted? exp)
  (tagged-list? exp 'quote))
(define (text-of-quotation exp) (cadr exp))
(define (tagged-list? exp tag)
  (if (pair? exp)
      (eq? (car exp) tag)
      false))

函数的求值

什么是函数表达式,其实就是著名的 lambda 表达式。lambda 表达式格式如下

(lambda <PARAM-LIST> <FUN-BODY>)

PARAM-LIST 是一个变量列表,引入变量的作用域,也叫环境。我们在求值 FUN-BODY 的时候,就在最内层的环境开始,有里向外的搜索变量。重名的时 候,内层变量先起作用,就这个就是所谓的阴影效果 (shadow) 。

求值一个 lambda 表达式的时候,返回值就是一个 “函数” 。

程序内部,如何表达一个函数呢?这也是一个重大的语言设计问题。编译型还是 解释型?目标语言的选择,字节码,还是机器码,还是其他的中间语言?

我们这个简单的语言中,我们就用 list 来表示。

(define (make-procedure parameters body env)
   (list 'procedure parameters body env))
(define (procedure-parameters p) (cadr p))
(define (procedure-body p) (caddr p))
(define (procedure-environment p) (cadddr p))

看看执行效果

> (make-procedure '(a b) 'a '())
(procedure (a b) a ())
> (set! p1 (make-procedure '(a b) 'a '()))
> (procedure-parameters  p1)
(a b)
> (procedure-body  p1)
a
> (procedure-env  p1)
> (procedure-environment  p1)
()

可见一个函数有三个元素,参数列表,函数体,和环境。环境这个概念一直没有 详细解释,再推到下面解释。

那么,我们先实现一个 lambda 表达式的解析。

(define (eval exp env)
  (cond (.......
        ((lambda? exp)
         (make-procedure (lambda-parameters exp)
                         (lambda-body exp)
                         env))
        ......

其他几个辅助函数的实现

(define (lambda? exp) (tagged-list? exp 'lambda))
(define (lambda-parameters exp) (cadr exp))
(define (lambda-body exp) (cddr exp))
(define (make-procedure parameters body env)
  (list 'procedure parameters body env))

看看执行效果

> (lambda? '(lambda 1))
#f
> (lambda-parameters '(lambda (a b) (+ a b)))
(a b)
> (lambda-body '(lambda (a b) (+ a b)))
((+ a b))
> (make-procedure '(a b) '((+ a b)) '())
(procedure (a b) ((+ a b)) ())
> (eval '(lambda (a b) (+ a b)) '())
(procedure (a b) ((+ a b)) ())

太好了,我们可以解析 lambda 表达式了。

这里是完整的代码

(define true #t)
(define false #f)
(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ((quoted? exp) (text-of-quotation exp))
        ((if? exp) (eval-if exp env))
        ((begin? exp)
         (eval-sequence (begin-actions exp) env))
        ((lambda? exp)
         (make-procedure (lambda-parameters exp)
                         (lambda-body exp)
                         env))
        (else (error "Unknown expression type -- EVAL" exp))))

(define (self-evaluating? exp)
  (cond ((number? exp) true)
        ((string? exp) true)
        (else false)))
(define (if? exp) (tagged-list? exp 'if))

(define (eval-if exp env)
  (if (true? (eval (if-predicate exp) env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))

(define (if-predicate exp) (cadr exp))
(define (if-consequent exp) (caddr exp))
(define (if-alternative exp)
  (if (not (null? (cdddr exp)))
      (cadddr exp)
      'false))
(define (true? exp)
  (eq? exp 'true))

(define (lambda? exp) (tagged-list? exp 'lambda))
(define (lambda-parameters exp) (cadr exp))
(define (lambda-body exp) (cddr exp))
(define (make-procedure parameters body env)
  (list 'procedure parameters body env))
(define (procedure-parameters p) (cadr p))
(define (procedure-body p) (caddr p))
(define (procedure-environment p) (cadddr p))

(define (eval-sequence exps env)
  (cond ((last-exp? exps) (eval (first-exp exps) env))
        (else (eval (first-exp exps) env)
              (eval-sequence (rest-exps exps) env))))

(define (begin? exp) (tagged-list? exp 'begin))
(define (begin-actions exp) (cdr exp))
(define (last-exp? seq) (null? (cdr seq)))
(define (first-exp seq) (car seq))
(define (rest-exps seq) (cdr seq))

(define (quoted? exp)
  (tagged-list? exp 'quote))
(define (text-of-quotation exp) (cadr exp))
(define (tagged-list? exp tag)
  (if (pair? exp)
      (eq? (car exp) tag)
      false))

函数调用与变量

刚刚我们定义了一个函数,但是并没有什么用处,我们看看什么是函数调用 (apply)。

(<L> <A1> <A2> ... <An>)

看看怎么解析这个函数调用型的 s-exp

  • 首先,L 是一个表达式,求值结果是一个函数
  • 然后,A1 是一个表达式,求值结果是任意值
  • 之后,依次求值 A2 ... An
  • 之后,绑定 (bind) A1 ... An 的值,到 L 的环境中。
  • 最后,对 L 的函数体求值。并返回结果

这里需要有两个注意的事项

  1. 求值顺序
  2. 递归调用
  3. 变量绑定

这是一门语言最核心的部分了。求值顺序。我似乎提到求值顺序很多次了。这里 选择了大多数主流语言的设计。

参数的求值顺序也很重要,有的语言为了性能,不定义参数的求值顺序。

第二个问题就是阴阳大法的互递归调用。apply 不停地调用 eval 得到函数对象, 和参数,绑定参数,然后调用 eval 求值函数体。而 apply 的处理,正式 eval 函数中的重要组成部分。

第三个问题就是变量的作用域的问题。变量的作用域也是一个重大的语言设计问 题。不同的语言有不同的设计,展现了不同语言丰富的表达能力。现在主流语言 是“词法作用域” (lexical scope) 。我们选择的是一个简单的词法作用域的实 现。

这里也需要注意参数的求值环境是在当前环境,而函数体的求值环境,是参数绑 定之后的新环境。

什么是求值环境?求值环境就是一个变量名称到变量值的映射关系。

什么是闭包(closure) ? 闭包也是一个环境,就是在生成一个函数的时候,当时 的环境。

什么是帧 (frame) ? 就是函数调用的时候,绑定参数生成的环境。

这就是说环境是一个动态的数状结构(递归结构)。在求值一个函数的函数体的 时候,碰到变量求值,会递归搜索闭包,这样除了参数绑定产生的本地环境之外, 还需要搜索闭包。

我们先实现 frame 。

(define (make-frame variables values)
  (cons variables values))
(define (frame-variables frame) (car frame))
(define (frame-values frame) (cdr frame))

看看运行效果

> (set! f1 (make-frame '(a b c) '(1 2 3)))
> (frame-variables f1)
(a b c)
> (frame-values f1)
(1 2 3)

我们再实现 env 。

(define (extend-environment vars vals base-env)
  (if (= (length vars) (length vals))
      (cons (make-frame vars vals) base-env)
      (if (< (length vars) (length vals))
          (error "Too many arguments supplied" vars vals)
          (error "Too few arguments supplied" vars vals))))
(define (enclosing-environment env) (cdr env))
(define (first-frame env) (car env))
(define the-empty-environment '())

看看执行效果

> (set! e1 (extend-environment '(a b c) '(1 2 3) the-empty-environment)))
> e1
(((a b c) 1 2 3))
> (enclosing-environment e1)
()
> (set! e2 (extend-environment '(x y z) '(10 20 30) e1))
> e2
(((x y z) 10 20 30) ((a b c) 1 2 3))
> (enclosing-environment e2)
(((a b c) 1 2 3))

我们再实现一个变量查找的功能

(define (lookup-variable-value var env)
  (define (env-loop env)
    (define (scan vars vals)
      (cond ((null? vars)
             (env-loop (enclosing-environment env)))
            ((eq? var (car vars))
             (car vals))
            (else (scan (cdr vars) (cdr vals)))))
    (if (eq? env the-empty-environment)
        (error "Unbound variable" var)
        (let ((frame (first-frame env)))
          (scan (frame-variables frame)
                (frame-values frame)))))
  (env-loop env))

我们看看执行效果

> (lookup-variable-value 'x e2)
10
> (lookup-variable-value 'y e2)
20
> (lookup-variable-value 'a e1)
1
> (lookup-variable-value 'a e2)
1
> (lookup-variable-value 'u e2)
Exception: Unbound variable with irritant u
Type (debug) to enter the debugger.

有了这些,我们可以实现变量的求值了。

(define (eval exp env)
  (cond (.......
        ((variable? exp) (lookup-variable-value exp env))
        ......

看看执行效果

> (eval 'a e1)
1
> (eval 'b e1)
2
> (eval 'c e1)
3
> (eval 'a e2)
1
> (eval 'b e2)
2
> (eval 'c e2)
3
> (eval 'x e2)
10
> (eval 'y e2)
20
> (eval 'z e2)
30

辅助函数的实现

(define (variable? exp) (symbol? exp))

我们继续,有了这些,我们可以实现 apply 的解析了

(define (eval exp env)
  (cond (.......
        ((application? exp)
         (apply (eval (operator exp) env)
                   (list-of-values (operands exp) env)))
        ......

看看一些辅助函数的实现

(define (application? exp) (pair? exp))
(define (operator exp) (car exp))
(define (operands exp) (cdr exp))
(define (no-operands? ops) (null? ops))
(define (first-operand ops) (car ops))
(define (rest-operands ops) (cdr ops))
(define (list-of-values exps env)
  (if (no-operands? exps)
      '()
      (cons (eval (first-operand exps) env)
            (list-of-values (rest-operands exps) env))))

看看执行效果

> (eval '((lambda (a b) a) 1 2) '())
1
> (eval '((lambda (a b) b) 1 2) '())
2
>

下面是完整的代码

(define true #t)
(define false #f)
(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ((quoted? exp) (text-of-quotation exp))
        ((if? exp) (eval-if exp env))
        ((begin? exp)
         (eval-sequence (begin-actions exp) env))
        ((lambda? exp)
         (make-procedure (lambda-parameters exp)
                         (lambda-body exp)
                         env))
        ((variable? exp) (lookup-variable-value exp env))
        ((application? exp)
         (apply (eval (operator exp) env)
                   (list-of-values (operands exp) env)))
        (else (error #f "Unknown expression type -- EVAL" exp))))

(define (self-evaluating? exp)
  (cond ((number? exp) true)
        ((string? exp) true)
        (else false)))
(define (if? exp) (tagged-list? exp 'if))

(define (eval-if exp env)
  (if (true? (eval (if-predicate exp) env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))

(define (if-predicate exp) (cadr exp))
(define (if-consequent exp) (caddr exp))
(define (if-alternative exp)
  (if (not (null? (cdddr exp)))
      (cadddr exp)
      'false))
(define (true? exp)
  (eq? exp 'true))

(define (lambda? exp) (tagged-list? exp 'lambda))
(define (lambda-parameters exp) (cadr exp))
(define (lambda-body exp) (cddr exp))
(define (make-procedure parameters body env)
  (list 'procedure parameters body env))
(define (procedure-parameters p) (cadr p))
(define (procedure-body p) (caddr p))
(define (procedure-environment p) (cadddr p))


(define (apply procedure arguments)
  (eval-sequence
   (procedure-body procedure)
   (extend-environment
    (procedure-parameters procedure)
    arguments
    (procedure-environment procedure))))

(define (extend-environment vars vals base-env)
  (if (= (length vars) (length vals))
      (cons (make-frame vars vals) base-env)
      (if (< (length vars) (length vals))
          (error "Too many arguments supplied" vars vals)
          (error "Too few arguments supplied" vars vals))))

(define (make-frame variables values)
  (cons variables values))
(define (frame-variables frame) (car frame))
(define (frame-values frame) (cdr frame))

(define (extend-environment vars vals base-env)
  (if (= (length vars) (length vals))
      (cons (make-frame vars vals) base-env)
      (if (< (length vars) (length vals))
          (error "Too many arguments supplied" vars vals)
          (error "Too few arguments supplied" vars vals))))
(define (enclosing-environment env) (cdr env))
(define (first-frame env) (car env))
(define the-empty-environment '())
(define (lookup-variable-value var env)
  (define (env-loop env)
    (define (scan vars vals)
      (cond ((null? vars)
             (env-loop (enclosing-environment env)))
            ((eq? var (car vars))
             (car vals))
            (else (scan (cdr vars) (cdr vals)))))
    (if (eq? env the-empty-environment)
        (error #f "Unbound variable" var)
        (let ((frame (first-frame env)))
          (scan (frame-variables frame)
                (frame-values frame)))))
  (env-loop env))

(define (variable? exp) (symbol? exp))
(define (application? exp) (pair? exp))
(define (operator exp) (car exp))
(define (operands exp) (cdr exp))
(define (no-operands? ops) (null? ops))
(define (first-operand ops) (car ops))
(define (rest-operands ops) (cdr ops))
(define (list-of-values exps env)
  (if (no-operands? exps)
      '()
      (cons (eval (first-operand exps) env)
            (list-of-values (rest-operands exps) env))))

(define (eval-sequence exps env)
  (cond ((last-exp? exps) (eval (first-exp exps) env))
        (else (eval (first-exp exps) env)
              (eval-sequence (rest-exps exps) env))))

(define (begin? exp) (tagged-list? exp 'begin))
(define (begin-actions exp) (cdr exp))
(define (last-exp? seq) (null? (cdr seq)))
(define (first-exp seq) (car seq))
(define (rest-exps seq) (cdr seq))

(define (quoted? exp)
  (tagged-list? exp 'quote))
(define (text-of-quotation exp) (cadr exp))
(define (tagged-list? exp tag)
  (if (pair? exp)
      (eq? (car exp) tag)
      false))

内置函数

我们有了语言的核心,但是这个语言还没什么用,因为连基本的加减乘除都做不 了。我们利用宿主语言提供的功能,实现一些基本的内置函数。

首先我们要区分内置函数和语言自己的函数。我们用 primitive 来标记内置函数。

(define (primitive-procedure? proc)
  (tagged-list? proc 'primitive))

primitive 函数有一个属性,就是底层的函数实现。

(define (primitive-implementation proc) (cadr proc))

我们预先定义下面这些内置函数

(define primitive-procedures
  (list (list 'car car)
        (list 'cdr cdr)
        (list 'cons cons)
        (list 'null? null?)
...
        ))

我们初始化语言的初始环境

(define (primitive-procedure-names)
  (map car
       primitive-procedures))

(define (primitive-procedure-objects)
  (map (lambda (proc) (list 'primitive (cadr proc)))
  primitive-procedures))
(define (setup-environment)
  (let ((initial-env
         (extend-environment (primitive-procedure-names)
                             (primitive-procedure-objects)
                             the-empty-environment)))
    initial-env))
(define the-global-environment (setup-environment))

看看执行效果

> (load "eval6.scm")
> the-global-environment
(((car cdr cons null?)
   (primitive #<procedure car>)
   (primitive #<procedure cdr>)
   (primitive #<procedure cons>)
   (primitive #<procedure null?>)))

对应的 apply 函数需要修改,这样才能调用内置函数。

(define (apply procedure arguments)
  (cond ((primitive-procedure? procedure)
         (apply-primitive-procedure procedure arguments))
        ((compound-procedure? procedure)
         (eval-sequence
           (procedure-body procedure)
           (extend-environment
             (procedure-parameters procedure)
             arguments
             (procedure-environment procedure))))
        (else
         (error
          "Unknown procedure type -- APPLY" procedure))))

看看其他几个辅助函数的实现

(define (compound-procedure? p) (tagged-list? p 'procedure))
(define (apply-primitive-procedure proc args)
  (apply-in-underlying-scheme
   (primitive-implementation proc) args))
(define apply-in-underlying-scheme apply)

看看执行效果

> (eval 'car the-global-environment)
(primitive #<procedure car>)
> (apply-primitive-procedure (eval 'car the-global-environment) '((1 2)))
1
> (eval '(car '(1 2)) the-global-environment)
1
> (eval '(display "hello world") the-global-environment)
hello world
>

这里有一个坑,apply 已经在宿主语言中有定义了,所有我们要换一个名字, 防止名字冲突。并且用 apply-primitive-procedure 强调我们调用的是宿主 语言的 apply 函数。

(define apply-in-underlying-scheme (top-level-value 'apply (scheme-environment)))

我们的语言居然可以输出 "hello world" 了。下面是完整的代码。

(define apply-in-underlying-scheme (top-level-value 'apply (scheme-environment)))
(define true #t)
(define false #f)
(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ((quoted? exp) (text-of-quotation exp))
        ((if? exp) (eval-if exp env))
        ((begin? exp)
         (eval-sequence (begin-actions exp) env))
        ((lambda? exp)
         (make-procedure (lambda-parameters exp)
                         (lambda-body exp)
                         env))
        ((variable? exp) (lookup-variable-value exp env))
        ((application? exp)
         (apply (eval (operator exp) env)
                   (list-of-values (operands exp) env)))
        (else (error #f "Unknown expression type -- EVAL" exp))))

(define (self-evaluating? exp)
  (cond ((number? exp) true)
        ((string? exp) true)
        (else false)))
(define (if? exp) (tagged-list? exp 'if))

(define (eval-if exp env)
  (if (true? (eval (if-predicate exp) env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))

(define (if-predicate exp) (cadr exp))
(define (if-consequent exp) (caddr exp))
(define (if-alternative exp)
  (if (not (null? (cdddr exp)))
      (cadddr exp)
      'false))
(define (true? exp)
  (eq? exp 'true))

(define (lambda? exp) (tagged-list? exp 'lambda))
(define (lambda-parameters exp) (cadr exp))
(define (lambda-body exp) (cddr exp))
(define (make-procedure parameters body env)
  (list 'procedure parameters body env))
(define (compound-procedure? p) (tagged-list? p 'procedure))
(define (procedure-parameters p) (cadr p))
(define (procedure-body p) (caddr p))
(define (procedure-environment p) (cadddr p))

(define (apply procedure arguments)
  (cond ((primitive-procedure? procedure)
         (apply-primitive-procedure procedure arguments))
        ((compound-procedure? procedure)
         (eval-sequence
           (procedure-body procedure)
           (extend-environment
             (procedure-parameters procedure)
             arguments
             (procedure-environment procedure))))
        (else
         (error
          "Unknown procedure type -- APPLY" procedure))))

(define (extend-environment vars vals base-env)
  (if (= (length vars) (length vals))
      (cons (make-frame vars vals) base-env)
      (if (< (length vars) (length vals))
          (error "Too many arguments supplied" vars vals)
          (error "Too few arguments supplied" vars vals))))

(define (make-frame variables values)
  (cons variables values))
(define (frame-variables frame) (car frame))
(define (frame-values frame) (cdr frame))

(define (extend-environment vars vals base-env)
  (if (= (length vars) (length vals))
      (cons (make-frame vars vals) base-env)
      (if (< (length vars) (length vals))
          (error "Too many arguments supplied" vars vals)
          (error "Too few arguments supplied" vars vals))))
(define (enclosing-environment env) (cdr env))
(define (first-frame env) (car env))
(define the-empty-environment '())
(define (lookup-variable-value var env)
  (define (env-loop env)
    (define (scan vars vals)
      (cond ((null? vars)
             (env-loop (enclosing-environment env)))
            ((eq? var (car vars))
             (car vals))
            (else (scan (cdr vars) (cdr vals)))))
    (if (eq? env the-empty-environment)
        (error #f "Unbound variable" var)
        (let ((frame (first-frame env)))
          (scan (frame-variables frame)
                (frame-values frame)))))
  (env-loop env))
(define (primitive-procedure? proc)
  (tagged-list? proc 'primitive))
(define primitive-procedures
  (list (list 'car car)
        (list 'cdr cdr)
        (list 'cons cons)
        (list 'null? null?)
        (list 'display display)
        ))
(define (primitive-implementation proc) (cadr proc))
(define (primitive-procedure-names)
  (map car
       primitive-procedures))
(define (apply-primitive-procedure proc args)
  (apply-in-underlying-scheme
   (primitive-implementation proc) args))


(define (primitive-procedure-objects)
  (map (lambda (proc) (list 'primitive (cadr proc)))
       primitive-procedures))
(define (setup-environment)
  (let ((initial-env
         (extend-environment (primitive-procedure-names)
                             (primitive-procedure-objects)
                             the-empty-environment)))
    initial-env))
(define the-global-environment (setup-environment))

(define (variable? exp) (symbol? exp))
(define (application? exp) (pair? exp))
(define (operator exp) (car exp))
(define (operands exp) (cdr exp))
(define (no-operands? ops) (null? ops))
(define (first-operand ops) (car ops))
(define (rest-operands ops) (cdr ops))
(define (list-of-values exps env)
  (if (no-operands? exps)
      '()
      (cons (eval (first-operand exps) env)
            (list-of-values (rest-operands exps) env))))

(define (eval-sequence exps env)
  (cond ((last-exp? exps) (eval (first-exp exps) env))
        (else (eval (first-exp exps) env)
              (eval-sequence (rest-exps exps) env))))

(define (begin? exp) (tagged-list? exp 'begin))
(define (begin-actions exp) (cdr exp))
(define (last-exp? seq) (null? (cdr seq)))
(define (first-exp seq) (car seq))
(define (rest-exps seq) (cdr seq))

(define (quoted? exp)
  (tagged-list? exp 'quote))
(define (text-of-quotation exp) (cadr exp))
(define (tagged-list? exp tag)
  (if (pair? exp)
      (eq? (car exp) tag)
      false))

定义变量(define) 和变量赋值(set!),还有 cond

这些都不是语言最核心的部分。这里给出完整的代码。

看看效果

> (eval '(begin (define a 10) a) the-global-environment)
10
> (eval '(begin (define a 10) (display a)) the-global-environment)
10
> (eval '(begin (define a 10) (+ a 100)) the-global-environment)
110
> (eval '(begin (define a 10) (set! a 1) (+ a 100)) the-global-environment)
101

最后完整的代码

;; 和宿主语言的交互
(define apply-in-underlying-scheme (top-level-value 'apply (scheme-environment)))
(define true #t)
(define false #f)
;; 语言的核心入口
(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ((variable? exp) (lookup-variable-value exp env))
        ((quoted? exp) (text-of-quotation exp))
        ((assignment? exp) (eval-assignment exp env))
        ((definition? exp) (eval-definition exp env))
        ((if? exp) (eval-if exp env))
        ((lambda? exp)
         (make-procedure (lambda-parameters exp)
                         (lambda-body exp)
                         env))
        ((begin? exp)
         (eval-sequence (begin-actions exp) env))
        ((cond? exp) (eval (cond->if exp) env))
        ((application? exp)
         (apply (eval (operator exp) env)
                (list-of-values (operands exp) env)))
        (else
         (error "Unknown expression type -- EVAL" exp))))

;; 简单的类型
(define (self-evaluating? exp)
  (cond ((number? exp) true)
        ((string? exp) true)
        (else false)))
;; 关于 quote
(define (quoted? exp)
  (tagged-list? exp 'quote))
(define (text-of-quotation exp) (cadr exp))

;; 关于代码块
(define (begin? exp) (tagged-list? exp 'begin))
(define (eval-sequence exps env)
  (cond ((last-exp? exps) (eval (first-exp exps) env))
        (else (eval (first-exp exps) env)
              (eval-sequence (rest-exps exps) env))))

(define (begin-actions exp) (cdr exp))
(define (last-exp? seq) (null? (cdr seq)))
(define (first-exp seq) (car seq))
(define (rest-exps seq) (cdr seq))

;; 支持 if 条件判断
(define (if? exp) (tagged-list? exp 'if))
(define (eval-if exp env)
  (if (true? (eval (if-predicate exp) env))
      (eval (if-consequent exp) env)
      (eval (if-alternative exp) env)))
(define (if-predicate exp) (cadr exp))
(define (if-consequent exp) (caddr exp))
(define (if-alternative exp)
  (if (not (null? (cdddr exp)))
      (cadddr exp)
      'false))
(define (true? exp)
  (eq? exp 'true))
;; 支持 lambda 函数定义
(define (lambda? exp) (tagged-list? exp 'lambda))
(define (lambda-parameters exp) (cadr exp))
(define (lambda-body exp) (cddr exp))
(define (make-procedure parameters body env)
  (list 'procedure parameters body env))
(define (compound-procedure? p) (tagged-list? p 'procedure))
(define (procedure-parameters p) (cadr p))
(define (procedure-body p) (caddr p))
(define (procedure-environment p) (cadddr p))
;; 关于环境的定义
(define (extend-environment vars vals base-env)
  (if (= (length vars) (length vals))
      (cons (make-frame vars vals) base-env)
      (if (< (length vars) (length vals))
          (error "Too many arguments supplied" vars vals)
          (error "Too few arguments supplied" vars vals))))
(define (make-frame variables values)
  (cons variables values))
(define (frame-variables frame) (car frame))
(define (frame-values frame) (cdr frame))
(define (extend-environment vars vals base-env)
  (if (= (length vars) (length vals))
      (cons (make-frame vars vals) base-env)
      (if (< (length vars) (length vals))
          (error "Too many arguments supplied" vars vals)
          (error "Too few arguments supplied" vars vals))))
(define (add-binding-to-frame! var val frame)
  (set-car! frame (cons var (car frame)))
  (set-cdr! frame (cons val (cdr frame))))
(define (enclosing-environment env) (cdr env))
(define (first-frame env) (car env))

(define primitive-procedures
  (list (list 'car car)
        (list 'cdr cdr)
        (list 'cons cons)
        (list 'null? null?)
        (list '+ +)
        (list '- -)
        (list '* *)
        (list '/ /)
        (list 'display display)
        ))
(define (primitive-procedure-names)
  (map car
       primitive-procedures))
(define (primitive-procedure-objects)
  (map (lambda (proc) (list 'primitive (cadr proc)))
       primitive-procedures))
(define (setup-environment)
  (let ((initial-env
         (extend-environment (primitive-procedure-names)
                             (primitive-procedure-objects)
                             the-empty-environment)))
    initial-env))
(define the-empty-environment '())
(define the-global-environment (setup-environment))

;; 关于变量
(define (variable? exp) (symbol? exp))
(define (lookup-variable-value var env)
  (define (env-loop env)
    (define (scan vars vals)
      (cond ((null? vars)
             (env-loop (enclosing-environment env)))
            ((eq? var (car vars))
             (car vals))
            (else (scan (cdr vars) (cdr vals)))))
    (if (eq? env the-empty-environment)
        (error #f "Unbound variable" var)
        (let ((frame (first-frame env)))
          (scan (frame-variables frame)
                (frame-values frame)))))
  (env-loop env))
;; 关于 apply 的相关函数
(define (application? exp) (pair? exp))
(define (apply procedure arguments)
  (cond ((primitive-procedure? procedure)
         (apply-primitive-procedure procedure arguments))
        ((compound-procedure? procedure)
         (eval-sequence
           (procedure-body procedure)
           (extend-environment
             (procedure-parameters procedure)
             arguments
             (procedure-environment procedure))))
        (else
         (error
          "Unknown procedure type -- APPLY" procedure))))
(define (primitive-procedure? proc)
  (tagged-list? proc 'primitive))
(define (primitive-implementation proc) (cadr proc))
(define (apply-primitive-procedure proc args)
  (apply-in-underlying-scheme
   (primitive-implementation proc) args))
(define (operator exp) (car exp))
(define (operands exp) (cdr exp))
(define (no-operands? ops) (null? ops))
(define (first-operand ops) (car ops))
(define (rest-operands ops) (cdr ops))
(define (list-of-values exps env)
  (if (no-operands? exps)
      '()
      (cons (eval (first-operand exps) env)
            (list-of-values (rest-operands exps) env))))

;; 关于 define
(define (definition? exp)
  (tagged-list? exp 'define))
(define (definition-variable exp)
  (if (symbol? (cadr exp))
      (cadr exp)
      (caadr exp)))
(define (definition-value exp)
  (if (symbol? (cadr exp))
      (caddr exp)
      (make-lambda (cdadr exp)   ; formal parameters
                   (cddr exp)))) ; body
(define (eval-definition exp env)
  (define-variable! (definition-variable exp)
                    (eval (definition-value exp) env)
                    env)
  'ok)
(define (define-variable! var val env)
  (let ((frame (first-frame env)))
    (define (scan vars vals)
      (cond ((null? vars)
             (add-binding-to-frame! var val frame))
            ((eq? var (car vars))
             (set-car! vals val))
            (else (scan (cdr vars) (cdr vals)))))
    (scan (frame-variables frame)
          (frame-values frame))))
;; 关于 set!
(define (assignment? exp)
  (tagged-list? exp 'set!))
(define (assignment-variable exp) (cadr exp))
(define (assignment-value exp) (caddr exp))
(define (eval-assignment exp env)
  (set-variable-value! (assignment-variable exp)
                       (eval (assignment-value exp) env)
                       env)
  'ok)
(define (set-variable-value! var val env)
  (define (env-loop env)
    (define (scan vars vals)
      (cond ((null? vars)
             (env-loop (enclosing-environment env)))
            ((eq? var (car vars))
             (set-car! vals val))
            (else (scan (cdr vars) (cdr vals)))))
    (if (eq? env the-empty-environment)
        (error "Unbound variable -- SET!" var)
        (let ((frame (first-frame env)))
          (scan (frame-variables frame)
                (frame-values frame)))))
  (env-loop env))
;; 关于 cond!
(define (cond? exp) (tagged-list? exp 'cond))
(define (cond-clauses exp) (cdr exp))
(define (cond-else-clause? clause)
  (eq? (cond-predicate clause) 'else))
(define (cond-predicate clause) (car clause))
(define (cond-actions clause) (cdr clause))
(define (cond->if exp)
  (expand-clauses (cond-clauses exp)))

(define (expand-clauses clauses)
  (if (null? clauses)
      'false                          ; no else clause
      (let ((first (car clauses))
            (rest (cdr clauses)))
        (if (cond-else-clause? first)
            (if (null? rest)
                (sequence->exp (cond-actions first))
                (error "ELSE clause isn't last -- COND->IF"
                       clauses))
            (make-if (cond-predicate first)
                     (sequence->exp (cond-actions first))
                     (expand-clauses rest))))))
;; 底层函数
(define (tagged-list? exp tag)
  (if (pair? exp)
      (eq? (car exp) tag)
      false))

其他

这个解释器,看起来简单,其实已经支持了大多数核心功能了,甚至包括闭包的 实现。一门语言还有其他一些重要的特性需要仔细设计。

  • 模块系统
  • 内存管理 gc
  • 代码生成优化
  • 调试器
  • 调优器
  • 特色功能
    • 是否支持 continuation
    • 是否支持 coroutine
    • 是否支持 thread
    • 是否支持 macro
    • 是否支持 类型推导
    • 是否支持 OO
    • 是否支持 泛型
    • 是否支持 generator/iterator
  • 标准库设计
    • Network/File IO
    • 基础数据类型库
      • List
      • Array
      • Map
      • Hash
      • Set

Erlang application environment

我们知道 Erlang 的函数 application:get_env/2 可以返回 app 的配置信息。 例如我们有一个简单的 app 文件,放在 ./ebin/myapp.ebin 中。

{application,myapp,
             [{description,[]},
              {vsn,"1"},
              {registered,[]},
              {applications,[kernel,stdlib]},
              {mod,{myapp_app,[]}},
              {env,[{a,1},{b,2}]},
              {modules,[myapp_app,myapp_server,myapp_sup]}]}.
bash$ erl -pa ebin
Erlang/OTP 18 [erts-7.0] [source] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V7.0  (abort with ^G)
1> application:load(myapp).
ok
2> application:get_env(myapp,a).
{ok,1}
3> application:get_env(myapp,b).
{ok,2}
4>

除此之外,我们可以通过命令行来修改 app env 。

bash$ erl -pa ebin -myapp a first b second
Erlang/OTP 18 [erts-7.0] [source] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V7.0  (abort with ^G)
1> application:get_env(myapp,a).
undefined
2> application:load(myapp).
ok
3> application:get_env(myapp,a).
{ok,first}
4> application:get_env(myapp,b).
{ok,second}
5>

注意到在 application:load(myapp) 之前,application:get_env(myapp) 都是返回 undefined

我们也注意到,命令行的配置,覆盖掉了原来 myapp.app 中的默认配置。

还有一种办法可以指定 app 环境,就是利用配置文件。例如,我们写一个配置文件 a.config,如下

[{myapp,
  [{a,one},
   {b,two},
   {c,yes}]}].

然后执行命令

bash$ erl -pa ebin -config a.config
Erlang/OTP 18 [erts-7.0] [source] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V7.0  (abort with ^G)
1> application:get_env(myapp,a).
undefined
2> application:load(myapp).
ok
3> application:get_env(myapp,a).
{ok,one}
4> application:get_env(myapp,b).
{ok,two}
5> application:get_env(myapp,c).
{ok,yes}
6>

可以看到,我们还可以指定新的参数, c

配置文件 a.config 可以 myapp.app 中的配置,命令行参数 -myapp a yi 可以覆盖配置文件中的设置。

bash$ erl -pa ebin -config a.config -myapp a yi
Erlang/OTP 18 [erts-7.0] [source] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V7.0  (abort with ^G)
1> application:load(myapp).
ok
2> application:get_env(myapp,a).
{ok,yi}
3> application:get_env(myapp,b).
{ok,two}

ZFS 转移数据

我以前有一个旧的磁盘阵列,用 zfs 系统,现在想把数据导入到新的系统里面。

数据传输速度很慢,下面这个链接很有帮助。

http://everycity.co.uk/alasdair/2010/07/using-mbuffer-to-speed-up-slow-zfs-send-zfs-receive/

安装 freenas 上的 mbuffer http://unquietwiki.blogspot.in/2015/01/mbuffer-on-freenas-sending-recursive.html

在目标机器上运行服务器。

[root@freenas2] ~# mbuffer -s 128k -m 3G -4 -I 9090 | zfs recv -v -F mypool/export

在源机器上运行客户端。

zfs snapshot -R create balbagvaba
c1@file1:~$ sudo zfs send -R mypool/export@20150708 | mbuffer -s 128k -m 1G -O 192.168.0.114:9090

查看系统瓶颈

top -SH
gstat

NFS 共享

freeNAS 没有使用 ZFS 的 NFS 共享,而是 freebsd 自带的 NFS 共享。修改 /etc/exports ,或者用图形界面。我的列表比较长,很容易用命令处理。

/mnt/mypool/export/prod
/mnt/mypool/export/cluster-root -maproot=root

参考 https://www.freebsd.org/doc/handbook/network-nfs.html

修改完了之后,运行

service mountd reload

查看是否生效

showmount -e localhost

C++11 的右值引用问题

C++11 中增加了一个新的特性,叫做“右值引用” (rvalue reference)。

主要参考文献

Abstract

Rvalue references is a small technical extension to the C++ language. Rvalue references allow programmers to avoid logically unnecessary copying and to provide perfect forwarding functions. They are primarily meant to aid in the design of higer performance and more robust libraries.

注解

  1. 为了避免无谓拷贝
  2. 为了提供完美转寄函数
  3. 主要目的是为了写通用库。

个人以为,一般应用程序的作者,尽量别用这个功能。

怎么理解这个功能呢?

首先理解什么是左值,右值。十分困难给出一个严格的定义,个人觉得一下非正 式定义比较容易理解。

  1. 能取地址的表达式,就是左值。
  2. 不是左值的表达式,就是右值。

最常见的右值

  1. 常量表达式 1 + 2,
  2. 函数调用参数的临时对象 f(A_Class())中 f 的实参。
  3. 函数调用的返回值产生的临时变量。

最简单的一个例子

#include <iostream>
using namespace std;
void foo(int && a)
{
    cout << __PRETTY_FUNCTION__ << ' ' << a << endl;
}
void foo(int& a)
{
    cout << __PRETTY_FUNCTION__ << ' ' << a << endl;
}
int bar()
{
    return 1000;
}
int main(int argc, char *argv[])
{
    int i = 100;
    foo(10);
    foo(i);
    foo(bar());
    return 0;
}

程序输出结果

% clang++ -ggdb -O0 -Wall -Werror -std=c++11 rvalue0.cpp && ./a.out
void foo(int &&) 10
void foo(int &) 100
void foo(int &&) 1000

普通函数调用重载理解了,就不难理解转移构造函数了。

谨慎使用这个功能

C++ 的编译器已经十分聪明,在一般情况下,已经尽最大可能避免无谓的对象拷 贝。例如

#include <iostream>
using namespace std;
struct A {
    A();
    ~A();
    A(const A& a);
    int value;
};
A::A()
{
    cout << __PRETTY_FUNCTION__ << " " << (void*) this << endl;
}
A::~A()
{
    cout << __PRETTY_FUNCTION__ << " " << (void*) this << endl;
}

A::A(const A& a)
{
    cout << __PRETTY_FUNCTION__ << " " << (void*) this << endl;
}
A bar()
{
    A ret;
    cout << __PRETTY_FUNCTION__ << " " << (void*) &ret << endl;
    return ret;
}
void foo(A x)
{
    cout << __PRETTY_FUNCTION__ << (void*)&x <<endl;
}
int main(int argc, char *argv[])
{
    foo(bar());
    return 0;
}

结果如下

% clang++ -ggdb -O0 -Wall -Werror -std=c++11 rvalue0.cpp && ./a.out
A::A() 0x7ffff687a968
A bar()
 0x7ffff687a968
void foo(A)0x7ffff687a968
A::~A() 0x7ffff687a968

左值引用是右值

void foo(A && x){
    ...
    x
}

在函数体中,x 不是右值,而是左值,因为可以取到 x 的地址。

std::move 的作用

这函数就是一个强制类型转换,把左值转换为左值,没有任何函数开销。如果没 有这个函数,一般很难调用转移构造函数(move constructor)。

可否返回一个临时对象的右值引用?

A&& foo()
{
     A x;
     return std::move(A)
     // or return x;
}

无论怎样,和返回普通引用一样,导致程序崩溃。

为什么有这个特性呢?

  1. 实现“可转移“ , 和 std::move 相关
  2. 提供完美转寄函数,和 std::forward 相关。(perfect forwarding function 怎么译?)

可转移

有很多时候,对象是可移动的而是不可以拷贝的。上面参考文献给出三个例子

  1. fstream
  2. unique_ptr (这里)[{{site.baseurl}}/{% post_url 2015-07-08-C-------feature--unique-ptr %}
  3. 线程对象

如果拷贝构造函数是私有函数,那么就是被禁止的。C++11 中有新特性,可以定 义 delete 函数。

一般来讲,一个对象拥有某一资源,例如内存,文件,线程等等,这种所有权是 不能与其他对象共享的,因为析构函数会释放资源,如果和别人共享,”别人“被 析构了,析构函数会释放资源,那么你手里就拿着一个被释放的资源。类似这样 的对象就是不可以拷贝的

但是_不可拷贝对象_并不是_不可转移对象_,假如拷贝了一个对象之后,但是源 对象放弃了所有权,那么目标对象就可以拥有所有权了。这个时候,我们不叫做 拷贝(copy),叫做转移(move)。

ifstream foo()
{
    ifstream fs(__FILE__);
    return fs;
}

这个例子会有编译错误,因为 ifstream 没有可用的拷贝构造函数,即“不可拷 贝”。但是在新的标准里面,解决了这个问题。 可惜的是 GCC-4.8 还没有实现 这个 ifstream 的转移构造函数。为了凑数,下面用 unique_ptr 为例。

#include <iostream>
#include <fstream>
#include <memory>
#include <iterator>
using namespace std;
unique_ptr<ifstream> foo()
{
    return std::move(unique_ptr<ifstream>(new ifstream(__FILE__)));
}
int main(int argc, char *argv[])
{
    unique_ptr<ifstream> fs = foo();
    copy(istreambuf_iterator<char>(*fs),istreambuf_iterator<char>(),
         ostream_iterator<char>(cout));
    return 0;
}

完美转寄函数(Perfect Forward function)

假设我们写一个函数 g(a1) ,就和调用 f(a1) 一模一样。很容易吗?

#include <iostream>
#include <vector>
using namespace std;
class A {
public:
    explicit A(int v);
    A(const A& other);
    ~A();
public:
    int value;
};
A::A(int v):value(v)
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
A::A(const A& other):value(other.value)
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::~A()
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
void f(A a)
{
    cout << a.value << endl;
}
template<class T>
void g(T a)
{
    f(a);
}

int main(int argc, char *argv[])
{
    A x(100);
    g<A>(x);
    return 0;
}

程序输出

A::A(int) 100 0x7fff48d239e8
A::A(const A &) 100 0x7fff48d239e0 0x7fff48d239e8
A::A(const A &) 100 0x7fff48d239a8 0x7fff48d239e0
100
A::~A() 100 0x7fff48d239a8
A::~A() 100 0x7fff48d239e0
A::~A() 100 0x7fff48d239e8

可以明显看到有一次多余拷贝构造函数。容易,改成引用就行了。

template<class T>
void g(T& a)
{
    f(a);
}

这样的确避免了这次多余的拷贝构造函数,但是就没有那么通用了。所以我的体 会是,这个 rvalue reference 的功能是给库作者用的,普通应用程序,不用搞 得这么复杂。

不通用的原因是,如果传递常量引用,就会编译错误。

const A x(100);
g<A>(x);

你会说,容易改,弄一个函数重载就行了。

template<class T>
void g(T& a)
{
    f(a);
}
template<class T>
void g(const T& a)
{
    f(a);
}

搞定。

这样是搞定了,但是如果这样的函数多了,写起来十分难看,可读性也很差。 c++11 rvalue reference 这样解决了这个问题。

#include <iostream>
#include <vector>
using namespace std;
class A {
public:
    explicit A(int v);
    A(const A& other);
    A(A&& other);
    ~A();
public:
    int value;
};
A::A(int v):value(v)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
A::A(const A& other):value(other.value)
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::A(A&& other):value(other.value)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::~A()
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
void f(A a)
{
    cout << a.value << endl;
}
template<class T>
void g(const T&& a)
{
    f(a);
}

int main(int argc, char *argv[])
{
    A x(100);
    g<A>(std::move(x));
    return 0;
}

写一个通用的完美转寄函数十分困难,标准已经弄好了。std::forward

如何使用 std::forward

参考 (std::forward)[http://en.cppreference.com/w/cpp/utility/forward]

template<class T>
void wrapper(T&& arg)
{
    foo(std::forward<T>(arg)); // Forward a single argument.
}
  1. 如果调用 wrapper 的实参是 std::string 类型的右值引用,那么 T 是 std::string (不是 std::string& 也不是 const std::string& 也 不是 std::string&&) ,那么 std::forward 保证传递右值引用给 foo 函数,即调用重载右值引用的 foo
  2. 如果调用 wrapper 的实参是 左值常量的 std::string ,那么 Tconst std::string& 并且 std::forward 保证传递常量左值引用给 foo 函数,即调用重载常量左值引用的 foo
  3. 如果调用 wrapper 的实参是非常量左值 std::string,那么 Tstd::string& ,并且 std::forward 保证传递非常量左值给 foo , 即调用重载非常量左值的 foo

听上去比较复杂,简单理解就是,怎么调用 wrapper ,就怎么调用 foo , 不用写很多的重载函数,只用一个 wrapper 函数,利用 std::forward , 就可以找到合适版本的 foo,就算 foo 有很多个重载版本。

#include <iostream>
#include <vector>
#include <string>
using namespace std;
void foo(std::string&& a)
{
    cout << __PRETTY_FUNCTION__ << " " <<  a << endl;
}
void foo(const std::string& a)
{
    cout << __PRETTY_FUNCTION__ << " " <<  a << endl;
}
void foo(string& a)
{
    cout << __PRETTY_FUNCTION__ << " " <<  a << endl;
}
template<class T>
void wrapper(T&& arg)
{
    foo(std::forward<T>(arg)); // Forward a single argument.
}

int main(int argc, char *argv[])
{
    string a = "lvalue";
    const string b = "const lvalue";
    wrapper(string("rvalue"));
    wrapper(b);
    wrapper(a);

    return 0;
}

输出结果

% lang++ -ggdb -O0 -Wall -Werror -std=c++11 forward3.cpp && ./a.out
void foo(std::string &&) rvalue
void foo(const std::string &) const lvalue
void foo(string &) lvalue

别重新发明轮子了,标准库已经做好了,而且这个工作并不简单。

不要用右值引用代替返回值优化

前面说了 C++11 rvalue reference 的两个用处。经常看到有人说,还有一个用 处,就是,如果一个函数返回一个临时变量, rvalue reference 可以提高性能。 我觉得这是一个误解, 返回值优化 (RVO) 已经是十分成熟的技术,不需要 rvalue reference ,有了这个,反而更慢。前面的例子已经看到,C++ 编译器 足够聪明,很多情况下已经避免了无谓拷贝。

#include <iostream>
#include <vector>
using namespace std;
class A {
public:
    explicit A(int v);
    A(const A& other);
    A(A&& other);
    ~A();
public:
    int value;
};
A::A(int v):value(v)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
A::A(const A& other):value(other.value)
{
    cout << __PRETTY_FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::A(A&& other):value(other.value)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::~A()
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
A f()
{
    return A(100);
}
int main(int argc, char *argv[])
{
    A x = f();
    return 0;
}

这个例子中

% clang++ -ggdb -O0 -Wall -Werror -std=c++11 rvo.cpp && ./a.out
A 100 0x7fff76a18a78
~A 100 0x7fff76a18a78

可以看到,没有任何多多余动作,RVO 工作的很好。我们画蛇添足一下,用 rvalue reference 看看。

改成

A && x = f()

程序输出没有任何变化。

如果

return A(100) => return std::move(A(100))

反倒多了一次转移构造函数的调用和临时对象的析构。

总结

  1. 右值引用是为了库函数作者用的,尤其是模板库。普通应用程序尽量不要用, 除非你真正理解这个功能。
  2. std::move 经常和转右值构造函数配合使用,转移构造函数的形参是右值引用。
  3. std::forward 利用右值引用。尽量用这个。

C++ vector 调用多少次元素的构造函数

使用 vector<A> 到底要调多少次构造函数A::A()呢?

#include <iostream>
#include <vector>
using namespace std;

class A {
public:
    explicit A(int v);
    explicit A(const A& other);
    ~A();
public:
    int value;
};
A::A(int v):value(v)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}
A::A(const A& other):value(other.value)
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << ' ' << (void*)&other
         << endl;
}
A::~A()
{
    cout << __FUNCTION__
         << ' ' << value
         << ' ' << (void*)this
         << endl;
}

vector<A> foo(int i)
{
    vector<A> x;
    for(int i = 0; i < 3; ++i){
        x.push_back(A(i));
    }
    cout << "foo return " << (void*) &x<< endl;
    return x;
}
int main(int argc, char *argv[])
{
    vector<A> x = foo(10);
    cout << "main get " << (void*) &x << endl;
    return 0;
}

运行结果 # 号后面的是我写的注释

clang++ -Wall -Werror -std=c++11 rvalue.cpp && ./a.out
# 构造临时对象 A(i), i = 0 ,地址是栈(stack)上的地址
A 0 0x7fff06edb8e0
# x.push_back(A(i)) 调用拷贝构造函数,other 的地址是刚刚创建的 A(i)
# 新对象x[0]的地址是 0x2528010,是在堆(heap)上的地址
A 0 0x2528010 0x7fff06edb8e0
# 调用析构函数,析构栈上的临时对象 x[0]
~A 0 0x7fff06edb8e0
# 和i=0类似 构造临时对象 A(i), i = 1,
# 因为是在栈上,自然重用刚刚 A(0),i=0 用过的地址。
A 1 0x7fff06edb8e0
# 和i=0类似 调用拷贝构造函数,准备 push_back
# x[1] 的地址是 0x2526034
A 1 0x2528034 0x7fff06edb8e0
# 糟糕,vector 发现内存不够了,动态扩展内存
# 用拷贝构造函数,把 x[0] 拷贝的新的位置上
# x[0] 的地址是 0x2528030,和 x[1] 是连续分配的,因为 sizeof(A) == 4
A 0 0x2528030 0x2528010
# 析构刚刚的 x[0]
~A 0 0x2528010
# 析构掉刚刚栈上的临时对象
~A 1 0x7fff06edb8e0
# 和 i=0 类似 构造临时对象 A(i), i = 2
A 2 0x7fff06edb8e0
# 和 i=0 类似 调用拷贝构造函数,准备 push_back
A 2 0x2528018 0x7fff06edb8e0
# 糟糕,内存又不够了,动态扩展内存
# 移动(move) x[0] 到新地址
A 0 0x2528010 0x2528030
# 移动(move) x[1] 到新地址
A 1 0x2528014 0x2528034
# 析构掉旧的 x[0]
~A 0 0x2528030
# 析构掉旧的 x[1]
~A 1 0x2528034
# 析构掉临时对象
~A 2 0x7fff06edb8e0
# foo() 返回 vector<A> 对象
foo return 0x7fffe5026248
# 因为有返回值优化,main 中x地址和 foo中x的地址是同一个地址。
main get 0x7fffe5026248
# main 函数返回,析构掉 vector 中的三个对象。
~A 0 0x2528010
~A 1 0x2528014
~A 2 0x2528018

如何避免多余的调用构造函数呢?提前申请内存!

    vector<A> x;
    x.reserve(10);
clang++ -Wall -Werror -std=c++11 vector_a.cpp && ./a.out
A 0 0x7ffffbf11a30
A 0 0x1044010 0x7ffffbf11a30
~A 0 0x7ffffbf11a30
A 1 0x7ffffbf11a30
A 1 0x1044014 0x7ffffbf11a30
~A 1 0x7ffffbf11a30
A 2 0x7ffffbf11a30
A 2 0x1044018 0x7ffffbf11a30
~A 2 0x7ffffbf11a30
foo return 0x7ffffbf11a88
main get 0x7ffffbf11a88
~A 0 0x1044010
~A 1 0x1044014
~A 2 0x1044018

可以看到,内存被提前申请了,所以不会有多余的拷贝构造函数了。

使用 vector<A>::push_back 之前,一定考虑调用 vector<A>::reserve

C++11 的 feature, unique_ptr

参考链接

  1. unique_ptr
  2. C++11: unique_ptr

unique_ptr 的语义

  1. unique_ptr <T> 看起来和指针 T* 很像
  2. 自动调用析构函数,释放所指对象的资源,不产生资源泄漏。
  3. unique_ptr<T> 保证只有一个对象拥有 (own) 某一个指针。

做到第一点很容易,重载 operator*operator->operator []即可。 很多人都会写。为了简单,unique_ptr<T> 不支持指针算数操作,++, -- 之类的。因为 unique_ptr<T> + 1 之后,他不能保证语义 2, 既他无法确定 他是 T* +1 的唯一拥有者。

#include <iostream>
#include <memory>
using namespace std;
struct A {
    A() {
        cerr <<  __FILE__ << ":" << __LINE__
             << ": [" << __FUNCTION__<< "] "
             << endl;
    }
    ~A() {
        cerr <<  __FILE__ << ":" << __LINE__
             << ": [" << __FUNCTION__<< "] "
             << endl;
    }
};
void test1()
{
    unique_ptr<A> ptr(new A());
}
int main(int argc, char *argv[])
{
    cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__<< "] "
         << "begin call test1"
         << endl;
    test1();
    cerr <<  __FILE__ << ":" << __LINE__ << ": [" << __FUNCTION__<< "] "
         << "done"
         << endl;
    return 0;
}

程序输出

% clang++ -std=c++11  -Wall -Werror unique_ptr.cpp && ./a.out
unique_ptr.cpp:20: [main] begin call test1
unique_ptr.cpp:6: [A]
unique_ptr.cpp:10: [~A]
unique_ptr.cpp:24: [main] done

第二点就有点麻烦,需要借用 C++11 的其他特性才能实现,最重要的就是右值 引用和 std::move

禁止拷贝构造函数

void test2_1(unique_ptr<A> x)
{
}
void test1()
{
    unique_ptr<A> ptr(new A());
    test2_1(ptr);
}

这样会产生编译错误

% clang++ -std=c++11  -Wall -Werror unique_ptr.cpp && ./a.out
unique_ptr.cpp:20:13: error: call to deleted constructor of 'unique_ptr<A>'
    test2_1(ptr);
            ^~~
/usr/bin/../lib/gcc/x86_64-linux-gnu/4.9/../../../../include/c++/4.9/bits/unique_ptr.h:356:7: note:
      'unique_ptr' has been explicitly marked deleted here
      unique_ptr(const unique_ptr&) = delete;
      ^
unique_ptr.cpp:14:28: note: passing argument to parameter 'x' here
void test2_1(unique_ptr<A> x)
                           ^
1 error generated.

禁止赋值操作符重载

void test1()
{
    unique_ptr<A> ptr(new A());
    unique_ptr<A> ptr2;
    ptr2 = ptr;
}

产生编译错误

% clang++ -std=c++11  -Wall -Werror unique_ptr.cpp && ./a.out
unique_ptr.cpp:18:10: error: overload resolution selected deleted operator '='
    ptr2 = ptr;

那他有啥用

不能复制,不能赋值,怎么用呢?我们经常用到传递指针的操作啊。

传递指针给另一个函数的时候,有两种情况

  1. 调用者保留指针的所有权
  2. 调用者放弃指针所有权

放弃所有权

#include <iostream>
#include <memory>
using namespace std;
struct A {
    A() {
        cerr <<  __FUNCTION__ << endl;
    }
    ~A() {
        cerr <<  __FUNCTION__ << endl;
    }
};
void test2(unique_ptr<A> ptr2)
{
    cerr << "test2 grab ownership" << endl;
    cerr << ptr2.get() << endl;
    cerr << "test2 done"<< endl;
}
void test1()
{
    unique_ptr<A> ptr(new A());
    cerr << "test1 give up ownership" << endl;
    test2(std::move(ptr));
    cerr << ptr.get() << endl;
}
int main(int argc, char *argv[])
{
    test1();
    return 0;
}

输出结果

% clang++ -std=c++11  -Wall -Werror unique_ptr.cpp && ./a.out
A
test1 give up ownership
test2 grab ownership
0xdfb010
test2 done
~A
0

可能有问题了,不是说不能调用拷贝构造函数吗?怎么 test2(std::move(ptr)) 就不报错了呢?这个不是拷贝构造函数,这个是移动 构造函数 (move constructor)。std::move 的作用是把一个变量变成右值引 用 (rvalue reference) 。这两个是 C++ 11 新的特性。

保留所有权

这个简单,就用 unique_ptr<A> & 代替就行了。这就是 unique*ptr 的作 用,如果 ptr 不放弃所有权,那么就不可能有其他的 unique*ptr<T> 拥有 同样的对象。 除非你传递 ptr 对象本身的引用。

和容器的关系

unique_ptr 可以很好的和容器一起工作。vector<unique_ptr<A>

#include <iostream>
#include <memory>
#include <vector>
using namespace std;
struct A {
    A(int _i):i(_i) {
        cerr <<  __FUNCTION__ <<  ' ' << i << endl;
    }
    ~A() {
        cerr <<  __FUNCTION__ << ' ' << i << endl;
    }
    int i;
};
void test1()
{
    cerr << "begin" << endl;
    {
        vector<unique_ptr<A> > v;
        for(int i = 0; i < 3; ++i){
            v.push_back(unique_ptr<A>(new A(i)));
        }
    }
    cerr << "done" << endl;
}
int main(int argc, char *argv[])
{
    test1();
    return 0;
}

这里隐含使用了右值引用,如果没有右值引用,实现这个功能还不容易啊。 v.push_back 实际上的函数原型是

void push_back( T&& value );

下面这个例子也行,使用到了转移构造函数(move constructor)

unique_ptr<A> ptr(new A(i));
v.push_back(std::move(ptr));

shared_ptr 的关系

unique_ptrshared_ptr 都可以自动析构动态生成的对象。但是二者不同。

  1. 多个 shared_ptr 对象可以共享同一个指针,而 unique_ptr 可以保证 我是指针的唯一拥有者。
  2. 当所有 shared_ptr 对象都被析构掉了,则动态指针所指的对象被析构。因 为 unique_ptr 唯一拥有该指针,unique_ptr 对象析构的时候,也会析构指 针所指的对象。

auto_ptr 的关系

unique_ptr 代替了 auto_ptrauto_ptr 是以前标准遗留的一个比较 难看的设计。为了避免抛出异常的时候,auto_ptr 的对象可以被成功析构。 但是因为缺少右值引用和移动构造函数的支持,auto_ptr 的语义很不清晰。 auto_ptr 也不能和容器很好的工作,你不能用 vector<auto_ptr<T> >

性能

unique_ptr 的性能很好,几乎没有任何多余的开销。参考2019-01-01-unique_ptr-的开销有多大

C/C++ 中的求值顺序

本文主要参考 Order of evaluation

举例如下

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int foo()
{
    printf("calling foo\n");
    return 1;
}
int bar()
{
    printf("calling bar\n");
    return 100;
}

int main(int argc, char *argv[])
{
    printf("%d %d",foo(),bar());
    return 0;
}

在用 clang编译的时候,输出如下

% clang seq.c && ./a.out
calling foo
calling bar
1 100

用 gcc 编译的时候,输出如下

% gcc seq.c && ./a.out
calling bar
calling foo
1 100

那到底是应该先调用 foo() 还是先调用 bar() ,在 C/C++ 标准里面,这是一 种未指定行为(unspecified)。但是对于某一种特定的编译器,这种行为是一 致的。这个和未定义(undefined)行为是有区别的。

表达式 f1() + f2() + f3() ,被解释为 (f1() + f2()) + f3()) ,但是 f3() 在哪里调用都有可能。

未定义行为

i = ++i + i++; // undefined behavior
i = i++ + 1; // undefined behavior (but i = ++i + 1; is well-defined)
f(++i, ++i); // undefined behavior
f(i = -1, i = -1); // undefined behavior
cout << i << i++; // undefined behavior
a[i] = i++; // undefined behavior

未定义行为的 C 语言里面的设计败笔。让人很难理解。我的体会是,别去惹他。 例如,你老老实实写成

int j = ++i;
int x = i++;
i = j +x

这样也挺好。

在极少的情况下,你需要依赖一种固定的求值顺序。

标准里面怎么说?

sequence point At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place. (§1.9/7)

执行顺序中有某些点,在这个点之前的表达式,因为求值产生的副作用(side effect),都应该被执行完毕。在这个点之后的表达式,因为求值产生的副作用, 不能发生。

什么是求值 evaluation

a = 1 + 2;

计算 1+2 = 3, 然后把 3 赋值给 a, 这个就是对一个表达式求值。

什么是 side effect 副作用。

对每一个表达式求值的时候,都有一个执行环境 (execution environment),在 执行环境里面,这个执行环境就是指所有的变量。有的变量在寄存器中,有的变 量在堆栈,有的在堆里面,有的在全局数据段中。对 C 语言的表达式,必定引 起对这个执行环境的变化,这种变化,就是 side effect。

a=1; // sequence point A

在 sequence point A 之后,我们可以保证,变量 a 发生的变化。

两个 sequence point 之间的求值顺序是没有定义的。

除了我们都熟悉的分号之后,是一个 sequence point 之外,还有一些 sequence point

  1. a&&b
  2. a||b
  3. a?b:c
  4. a,b 注意这里不是函数调用,而是一个不常用的逗号操作符。
// sequence point A
cout << i << i++;
// sequence point B

在 A B 之间的求值顺序是没有保证的。

//sequence point A
printf("%d", foo(),bar());
//sequence point B

//sequence point A
i = i++ + ++i;
//sequence point B

layout: post title: "C/C++ 编程风格: if-else" date: 2015/07/08 10:50:45 categories: comments: true

我的习惯是直接写成下面这样的逻辑。

{% highlight c++ %} if(a==0 && b == 0){ }else if(a!=0 && b == 0){ }else if (a== 0 && b!=0){ }else if(a!=0 && b!=0) { }else{ assert(0 && "never goes here"); } {% endhighlight %}

原则

  1. 尽量少使用 if 语句
  2. 尽量减少 then, else 中的语句。
  3. 如果使用,条件尽量简单,尽量不适用复合条件判断。
  4. 如果必须使用复合条件判断,一定包含所有可能性。
  5. if 语句一定要有 else 语句相随,就算 else 里面啥也没有,也要写一个空 的括号。

有一种常用技巧可以减少 if 语句的使用,例如

{% highlight c++ %} if(cmd == 0){ ret = do_cmd_0(); }else if(cmd == 1){ ret = do_cmd_1() } .... else { asset(0 && "never goes here"); }

or

switch(cmd){ case 0: ret = do_cmd_0(); break; case 1: ret = do_cmd_0(); break; default: assert(0 && "never goes here"); } return ret; {% endhighlight %}

可以写成

{% highlight c++ %} cmd_table[] = { do_cmd_0, do_cmd_1, ... };

ret = cmd_tablecmd; {% endhighlight %}

如果是

{% highlight c++ %} if (a == 0){ do_something_with(1,2,"good"); }else{ do_something_with(1,2,"bad"); } {% endhighlight %}

就可以写成

{% highlight c++ %} do_something_with(1,2,a==0?"good":"bad"); {% endhighlight %}

以尽量缩小 if 语句的控制范围。

真实的代码里面,

{% highlight c++ %} if(a==0){ do_something_xxx(1,2,3); do_other(); }else{ do_something_yyy(4,5,6); do_other(); } {% endhighlight %}

如果是

{% highlight c++ %} if(a==0) { blbbnalla very very long }else { yalayalaya very very long } {% endhighlight %}

一定要把 then 和 else 的内容提各自取成一个函数,不要让 then else 太长。

if else 看似很简单,实际上对代码的可读性有很大的影响。这些改进都是很简 单的改进,小小的改进,积累多了,就清爽多了。如果看不起这些小小的改进, 臭代码积累多了,以后再改就难了。

编程风格: C++ 类的结构

如果一个类比较复杂,我会按照下面的顺序写一个类。

class A : public interface{
public: // 类静态函数
     static class_method1() ; //
private:
     static class_priate_method1() ; // 尽量不要有
public:
     explicit A(); // 尽量使用 explicit
private:
     // 没有拷贝构造函数,就显式禁止拷贝构造函数,如果有,则显示定义
     A( const A& x);
     // 禁止 = 操作符重载。如果有,则显示定义。
     A& operator=(const A& other);
public:
     virtual // 这里一定想好,是不是应该有一个 virtual
     ~A();
private:
     // 实现父类的公有接口。
     virtual void foo();
private:
     // 类私有函数
private:
     // 真正的类私有变量
};

// 尽量不要使用,protected
// 尽量不要使用超过三层以上的继承关系。

详细展开讨论

类方法 (class method, static member function)

这个不属于类对象,通常是一些构造对象用的函数。

class A_interface {
public:
     static A * create();
public:
     virtual void interface1() = 0;
     virtual void interface2() = 0;
};

这是一个比较好的父类结构

  • 类没有成员变量。
  • 类没有构造函数,因为没有成员变量。
  • 类没有私有函数,没有非虚函数。
  • 只有一个 类方法 Create()
  • 从用户的角度上看,这个结构一目了然,调用
A_interface * obj= A_interface::Create();
obj->interface1();
obj->interface2();

构造函数

构造函数尽量使用 explicit 的关键字。这个关键字可以阻止意想不到的构造函 数调用。

拷贝构造函数

如果不想使用拷贝构造函数,给他弄成 private ,这样的好处是,无论是谁, 包括父类,子类,还是用户,都不能访问。拷贝构造函数在很多时候,隐含调用, 增加了调试难度,同时也是 bug 产生的发源地。这样做的之后,又如下限制,

不能声明这样的函数 Foo(A x) ,因为这个需要拷贝构造函数调用。同样原理, 不能使用 vector<A> 等容器。我体会实际使用中,这样类型的类还占大多数。 很多情况下,都是指针或者应用传递才有意义。赋值操作符重载

这个也是 bug 的发源地。原理同拷贝构造函数类似。

析构函数

如果类里面有虚函数,析构函数一定要是虚函数。

虚函数的实现

尽管父类的虚函数是 public ,但是在子类实现的时候,最好是 private

一个例子

我写一个例子程序说明我的想法。这个程序是 busybox ,就是用一个程序实 现很多 unix 下常用的命令。

首先创建一个 cmd.h

#pragma once
#include <string>
// using namespace std
class cmd_c {
public:
    static cmd_c * create(std::string cmd);
public:
    virtual ~cmd_c();
public:
    virtual int execute(int argc, char * argv[]) = 0;
};
  • #progam once 代替

    #ifndef CMD_H
    #define CMD_H
    ....
    #endif
    

    #program once 的好处是,短小。不用三行

    也不用担心 CMD_H 宏重命名的问题。如果你有连个头文件,一不小心(概率 很小),两个头文件用同样的宏保护,而且两个头文件同时被引用,就会出问题。

    坏处就是,这个不是所有的编译器都支持,但是幸运的是,几乎所有的编译器都 支持,包括 vs, gcc, clang 等等。

  • 不要使用 using namespace std

    在头文件的定义中,尽量不要 using namespace std 这样会引起命名空间污 染。如果你觉得 std::stringstring 长,每次输入 std:: 很麻 烦,可以在 cpp文件中,using namespace std 。头文件可能被其他用户 引用,其他用户也许不想使用 namespace std

  • virtual ~cmd_c 很重要,保证析构函数是虚函数,否则不能调用子类的析 构函数。

  • 不用禁止构造函数。

    因为 execute 都是纯虚函数,就不要声明私有的构造函数了。因为如果一 个类有纯虚函数,则不能构造对象。因为只能通过 create 函数创建 cmd_c 的子类对象,否则无法创建一个 cmd_c 的对象。这就是纯虚函数 的好处。

  • 初学 C++ 的,很容易忽略纯虚函数的作用。

创建 main.cpp

#include "cmd.h"
using namespace std;
int main(int argc, char *argv[])
{
    cmd_c * cmd = cmd_c::create(string(argv[0]));
    int ret = cmd->execute(argc,argv);
    delete cmd;
    return ret;
}

从用户的角度上看 cmd_c 这个类,十分清爽。

创建 cmd_args.h ,第一个 cmd_c 的实现

#pragma once
#include "cmd.h"
class cmd_args_c: public cmd_c {
public:
    static cmd_c * create();
private:
    explicit cmd_args_c();
    explicit cmd_args_c(const cmd_args_c&);
    cmd_args_c& operator= (const cmd_args_c&);
    virtual  ~cmd_args_c();
private:
    virtual int execute(int argc, char * argv[]);
};
  • 禁止了构造函数,拷贝构造函数,赋值操作符重载。
  • 也禁止了析构函数。因为父类的析构函数是虚函数,所以子类可以重载虚函数。

创建 cmd_args.cpp

#include <iostream>
using namespace std;
#include "cmd.h"
#include "cmd_args.h"
cmd_args_c::cmd_args_c()
{
}
cmd_args_c::cmd_args_c(const cmd_args_c&)
{
}
cmd_args_c& cmd_args_c::operator= (const cmd_args_c&)
{
    return *this;
}
cmd_args_c::~cmd_args_c()
{
}
cmd_c * cmd_args_c::create()
{
    return new cmd_args_c();
}
int cmd_args_c::execute(int argc, char* argv[])
{
    for(int i = 0; i < argc; ++i){
        cout << "argv[" << i << "]:" << argv[i] << endl;
    }
    return 0;

}

创建 cmd.cpp,定义如何创建具体对象

#include <cassert>
#include "cmd.h"
#include "cmd_args.h"
using namespace std;

cmd_c::~cmd_c()
{
}
static struct {
    const char * name;
    cmd_c* (*func)();
} command_table [] = {
    {"./args", cmd_args_c::create },
    {NULL,NULL}
};
cmd_c * cmd_c::create(string cmd)
{
    cmd_c * ret = NULL;
    for(int i = 0; command_table[i].name !=NULL; ++i){
        if(cmd == command_table[i].name){
            ret = command_table[i].func();
            break;
        }
    }
    assert(ret);
    return ret;

}

在 cmd.cpp 里面可以放心大胆的使用 using namespace std ,爱怎么弄怎么弄。 没有名字污染的问题。

cmd.cpp 里面要定义 ~cmd() 否则链接的时候会报错

cmd_args.cpp:(.text+0xb1): undefined reference to `cmd_c::~cmd_c()'

定义 command_table ,这里使用了匿名结构体。因为只有这一个全局变量, 可以避免名字空间污染,也可以避免为了起名浪费脑细胞。

command_table 是 static 变量,遵循原则,“尽量减少变量的作用域”,

为了遵循这个原则,是不是可以把 command_table 放在 cmd_c::create 的 内部。

cmd_c * cmd_c::create(string cmd)
{
   static .... command_table ...;
}

这样违反了另一个原则,“分离数据和实现” 。这里是用来注册子类的构造方法 的地方, cmd_c 的子类实现者要很容易找到在哪里注册一个新的子类实现, 而不用关心 cmd_c::create 的具体实现了。

cmd_c::create 的结尾用 assert(0) 表示没有实现的功能 ,如果以后写得 复杂了,需要处理如果命令找不到怎么办。这里为了简单,就不处理了。

编译之,哈哈

因为有多个 cpp 的文件,写一个简单的 Makefile

CXXFLAGS += -Wall -Werror -Wextra
all: busybox

busybox: main.o cmd.o cmd_args.o
	$(CXX) -o $@ $+

注意: -Wall -Werror -Wextra 是一个很好的习惯,尽量尽量好准守。我用 了两个“尽量”。项目初期就加上这个,其实影响很小,到了项目后期,如果除掉 所有的 warning 还是很麻烦的事。一个好的程序员,对于 warning 应该产生罪 恶感。

使用 make

% make
% clang++ -Wall -Werror -Wextra   -c -o main.o main.cpp
% clang++ -Wall -Werror -Wextra   -c -o cmd.o cmd.cpp
% clang++ -Wall -Werror -Wextra   -c -o cmd_args.o cmd_args.cpp
% clang++ -o busybox main.o cmd.o cmd_args.o

运行

bash$ ln -s ./busybox args
bash$ ./args
argv[0]:./args
bash$ ./args show me
argv[0]:./args
argv[1]:show
argv[2]:me

创建一个符号连接到 busybox 上,这个是 Linux 下的通用做法。保证 "./args" 是第一个参数 argv[0]

运行结果就自说明了。

这只是一个例子,很容易处理其他情况,例如,

  • 不在当前目录运行的时候,argv[0] 不是 "./args‘怎么处理,
  • 命令找不到怎么处理。
  • 怎么添加一个新的命令。例如 cmd_ls_c, cmd_cp_c

其他问题:

  • cmd.cpp 依赖于所有子类的实现,怎么破?
  • cmd_args_c::create 没有参数,如果要传递参数怎么破?

折腾 FreeBSD

据说不喜欢 Windows 的人使用 Linux, 而喜欢 Unix 的人使用 FreeBSD。我没 有啥偏见,就是好奇,试着玩一玩 FreeBSD。

首先我下载了一个虚拟机版本的 FreeBSD 映像。 下载地址 。我用的是 VirtualBox 24.3.26 ,下载的是 vhd 格式。下载过 vmdk 格式的, 但是 virtualbox 出现非法操作。

然后用 virtualbox 创建一个虚拟机,打开虚拟镜像。

如何设置主机名称

sysrc hostname="mybsd"

打开网卡

ifconfig em0 up
dhclient em0

如何安装软件

pkg install vim

pkg 安装的时候出现 "operation timedout", 因为我的网络连接太慢了,我看 到 man fetch 里面提到,可以用环境变量调整这个超时时间。

setenv HTTP_TIMEOUT 3000 # 我花了好一段时间找到窍门。

安装 guest addition

pkg install virtualbox-ose-additions-4.3.28
sysrc vboxguest_enable=yes
sysrc vboxservice_enable=yes

然后重启

如何查看有 rc.conf 中所有的默认配置

sysrc -A

如何查找软件

pkg search gdm
pkg search --description 'display manager'

参考

  • https://www.freebsd.org/doc/handbook/ports-finding-applications.html
  • man pkg-search

安装图形系统

pkg install xorg xfce4
pkg install gdm xorg xfce4
sysrc gdm_enable=YES
sysrc gnome_enable=YES
sysrc hald_enable="YES"  # 尤其是这两句话
sysrc dbus_enable="YES"

修改 /etc/fstab

proc /proc procfs rw 0 0

严格按照这几个步骤,否则图形界面出不来,还没有错误输出。查看 /var/log/gdm/:0-greater.log ,提示找不到 gnome-shell ,于是安装 gnome-shell ,其实我不太喜欢这个 gnome-shell ,被迫安装之。

pkg install gnome-shell

图形界面出来了,但是还黑屏幕。最后看 /var/log/gdm/:0-greater.log 可以 看到缺少 theme ,我也不愿在寻找这些 themes 了。这里 FreeBSD 不是很好。 gdm 太臃肿了。放弃了。

换 slim

pkg install slim
sysrc slim_enable=YES

重启,一切正常,可是登陆以后,屏幕一闪就没有了。这个很明显是 xinitrc 没有配置。

echo "exec /usr/local/bin/xfce4-session" > ~/.xinitrc
chmod +x ~/.xinitrc

参考

  • https://www.freebsd.org/doc/handbook/x11-wm.html

安装 ssh

pkg install openssh-portable
sysrc sshd_enable=YES
service sshd start

安装常用软件

pkg install gdm xfce4 emacs24 vim gcc git subversion

概念学习: 服务

  • /etc/rc.d 下面有很多 shell 脚本,控制服务。
  • rcvar 表示控制服务的变量
  • /etc/rc.conf 里面可以设置变量
  • service 命令可以启动,停止,重启这些服务。 具体查看 man service
  • sysrc 命令可以控制 /etc/rc.conf 中的变量设置。具体查看 man sysrc

参考文献

  • https://www.freebsd.org/doc/handbook/configtuning-starting-services.html

体会

  • 很干净。例如 debian 安装 openssh 之后,默认打开 sshd 服务,不需要任 何配置。freeBSD 需要简单的配置。难说那一个好,那一个坏。
  • Debian 安装配置快速。缺点是如果你不懂的话,不知道到哪里去停止服务。
  • FreeBSD 安装之后不能用。但是如果你懂的话,很容易知道怎么配置,一旦知 道怎么配置,发现其他的服务的方法都是类似的。
  • GDM 这个是 GDM 本身的问题,太过于臃肿。
  • 据说 FreeBSD 上的商用软件似乎还不多,硬件支持不好。这个和市场占有率 有关系,不过我就是做一个稳定的开发环境,和我关系不大。
  • 以后的体会在慢慢说了。.....

C Preprocessor tricks

In this blog, I will show some examples about advanced C Preprocessor examples. First of all, I would say CPP macros are ugly, hard to debug and error prone.

define a list of items

I would like to use an example to illustrate the idea behind it. For example, I would like to implementat a set of unix command as below

#define CMD(XX) XX(ls) XX(cp) XX(rm) XX(echo)

Then I could be able to write a common template function as below.

#define DEFINE_COMMON_TEMPLATE(name)            \
void name()                                     \
{                                               \
   printf(#name " is not implemented\n");       \
}

CMD(DEFINE_COMMON_TEMPLATE)

It will expanded to

void ls ()
{
  printf ("ls" " is not implemented\n");
}

void cp ()
{
  printf ("cp" " is not implemented\n");
} // ...

In the main function, we can use the similiar trick, as below

#define COMMAND_SWITCH(name)                    \
    if (strcmp(argv[1], #name) == 0){           \
        name();                                 \
    } else
    CMD(COMMAND_SWITCH) {
        printf("unknown commmand %s\n", argv[1]);
    }

And it will expanded into

  if (strcmp (argv[1], "ls") == 0)
    {
      ls ();
    }
  else if (strcmp (argv[1], "cp") == 0)
    {
      cp ();
    }
  else if (strcmp (argv[1], "rm") == 0)
    {
      rm ();
    }
  else if (strcmp (argv[1], "echo") == 0)
    {
      echo ();
    }
  else
    {
      printf ("unknown commmand %s\n", argv[1]);
    }

The whole program is listed here.

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

#define CMD(XX) XX(ls) XX(cp) XX(rm) XX(echo)

#define DEFINE_COMMON_TEMPLATE(name)            \
void name()                                     \
{                                               \
   printf(#name " is not implemented\n");       \
}

CMD(DEFINE_COMMON_TEMPLATE)

int main(int argc, char *argv[])
{
#define COMMAND_SWITCH(name)                    \
    if (strcmp(argv[1], #name) == 0){           \
        name();                                 \
    } else

    CMD(COMMAND_SWITCH) {
        printf("unknown commmand %s\n", argv[1]);
    }
    return 0;
}

get the number of varadic argument

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#define _GET_N_OF_ARGS(_0,_1,_2,_3,_4,N,...) N
#define N_OF_ARGS(...) _GET_N_OF_ARGS(__VA_ARGS__,5,4,3,2,1,0)
int main(int argc, char *argv[])
{
    printf("%d\n", N_OF_ARGS(a));
    printf("%d\n",N_OF_ARGS(a,b));
    printf("%d\n",N_OF_ARGS(a,b,c));
    printf("%d\n",N_OF_ARGS(a,b,c,d));
    printf("%d\n",N_OF_ARGS(a,b,c,d,e));
    return 0;
}

N_OF_ARGS can handle at most 5 arguments and at least 1, otherwise, it is unpredictable error.

int main(int argc, char *argv[])
{
    printf("%d\n",1);
    printf("%d\n",2);
    printf("%d\n",3);
    printf("%d\n",4);
    printf("%d\n",5);
    return 0;
}

But

N_OF_ARGS()
// => _GET_N_OF_ARGS(,5,4,3,2,1)
// => 1
printf("%d\n",N_OF_ARGS(a,b,c,d,e,f));
// => _GET_N_OF_ARGS(a,b,c,d,e,f)
// => f

get the nth element of arguments

C++ should support C99 designated initializer

Two Core C99 Features that C++11 Lacks mentions "Designated Initializers and C++".

I think the 'designated initializer' related with potential optimization. Here I use "gcc/g++" 5.1 as an example.

g++ (GCC) 5.1.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

gcc (GCC) 5.1.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Here is a simple example.

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

struct point {
    int x;
    int y;
};
const struct point a_point = {.x = 0, .y = 0};
int foo() {
    if(a_point.x == 0){
        printf("x == 0");
        return 0;
    }else{
        printf("x == 1");
        return 1;
    }
}
int main(int argc, char *argv[])
{
    return foo();
}

We knew at compilation time, a_point.x is zero, so we could expected that foo is optimized into a single printf.

$ gcc -O3 a.c
$ gdb a.out
(gdb) disassemble foo
Dump of assembler code for function foo:
   0x00000000004004f0 <+0>:	sub    $0x8,%rsp
   0x00000000004004f4 <+4>:	mov    $0x4005bc,%edi
   0x00000000004004f9 <+9>:	xor    %eax,%eax
   0x00000000004004fb <+11>:	callq  0x4003a0 <printf@plt>
   0x0000000000400500 <+16>:	xor    %eax,%eax
   0x0000000000400502 <+18>:	add    $0x8,%rsp
   0x0000000000400506 <+22>:	retq
End of assembler dump.
(gdb) x /s 0x4005bc
0x4005bc:	"x == 0"

Very good! foo is optmized to print x == 0 only.

Let's see c++ version.

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

struct point {
    point(int _x,int _y):x(_x),y(_y){}
    int x;
    int y;
};
const struct point a_point(0,0);
int foo() {
    if(a_point.x == 0){
	printf("x == 0");
	return 0;
    }else{
	printf("x == 1");
	return 1;
    }
}
int main(int argc, char *argv[])
{
    return foo();
}

$ g++ -O3 a.cc
$ gdb a.out
(gdb) disassemble foo
Dump of assembler code for function _Z3foov:
   0x00000000004005c0 <+0>:	push   %rbx
   0x00000000004005c1 <+1>:	mov    0x200489(%rip),%ebx        # 0x600a50 <_ZL7a_point>
   0x00000000004005c7 <+7>:	test   %ebx,%ebx
   0x00000000004005c9 <+9>:	je     0x4005e0 <_Z3foov+32>
   0x00000000004005cb <+11>:	mov    $0x1,%ebx
   0x00000000004005d0 <+16>:	mov    $0x4006a3,%edi
   0x00000000004005d5 <+21>:	xor    %eax,%eax
   0x00000000004005d7 <+23>:	callq  0x400460 <printf@plt>
   0x00000000004005dc <+28>:	mov    %ebx,%eax
   0x00000000004005de <+30>:	pop    %rbx
   0x00000000004005df <+31>:	retq
   0x00000000004005e0 <+32>:	mov    $0x40069c,%edi
   0x00000000004005e5 <+37>:	xor    %eax,%eax
   0x00000000004005e7 <+39>:	callq  0x400460 <printf@plt>
   0x00000000004005ec <+44>:	mov    %ebx,%eax
   0x00000000004005ee <+46>:	pop    %rbx
   0x00000000004005ef <+47>:	retq
End of assembler dump.

We can see that a_point is not really a compile time constant value.

C++ virtual function

How C++ virtual function is implemented? Compilers have their own implementations. I am interested to see the implementation of gcc.

$ gcc --version
gcc (Debian 4.7.2-5) 4.7.2
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

This is a simple example.

class B {
public:
    virtual int foo1() = 0;
    virtual int foo2() = 0;
    virtual int foo3() = 0;
};

int bar(B * obj)
{
    return obj->foo3();
}

Here is the output of the assemble code.

$ gcc -O3 -S -c -o - vf.cc
        .file   "vf.cc"
        .text
        .p2align 4,,15
        .globl  _Z3barP1B
        .type   _Z3barP1B, @function
_Z3barP1B:
.LFB0:
        .cfi_startproc
        movq    (%rdi), %rax
        movq    16(%rax), %rax
        jmp     *%rax
        .cfi_endproc
.LFE0:
        .size   _Z3barP1B, .-_Z3barP1B
        .ident  "GCC: (Debian 4.7.2-5) 4.7.2"
        .section        .note.GNU-stack,"",@progbits

The interesting part is

        movq    (%rdi), %rax
        movq    16(%rax), %rax
        jmp     *%rax

We know %rdi is the first function argument, and it is this pointer. I guess the very first 8 bytes it is pointed to are the pointer of vtable, so firstly, we load the vtable into %rax. gcc knows that foo3 is the third virtual function, so that the offset of foo3 is 16, and we load the virtual function pointer foo3 into %rax. Finally, jumping to the virtual function.

Understand X86 64 calling convention

Calling convention

System V AMD64 ABI

The calling convention of the System V AMD64 ABI[14] is followed on Solaris, Linux, FreeBSD, Mac OS X, and other UNIX-like or POSIX-compliant operating systems. The first six integer or pointer arguments are passed in registers RDI, RSI, RDX, RCX, R8, and R9, while XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 and XMM7 are used for floating point arguments. For system calls, R10 is used instead of RCX.[14] As in the Microsoft x64 calling convention, additional arguments are passed on the stack and the return value is stored in RAX.

Registers RBP, RBX, and R12-R15 are callee-save registers; all others must be saved by the caller if they wish to preserve their values.[15]

#include <stdint.h>
typedef int64_t i64;
i64 foo(i64 a0,i64 a1,i64 a2,i64 a3,i64 a4,
	i64 a5,i64 a6,i64 a7,i64 a8,i64 a9)
{
    return a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9;
}
int main(int argc, char *argv[])
{
    foo(0,1,2,3,4,5,6,7,8,9);
    return 0;
}
   0x0000000000400526 <+15>:	pushq  $0x9
   0x0000000000400528 <+17>:	pushq  $0x8
   0x000000000040052a <+19>:	pushq  $0x7
   0x000000000040052c <+21>:	pushq  $0x6
   0x000000000040052e <+23>:	mov    $0x5,%r9d
   0x0000000000400534 <+29>:	mov    $0x4,%r8d
   0x000000000040053a <+35>:	mov    $0x3,%ecx
   0x000000000040053f <+40>:	mov    $0x2,%edx
   0x0000000000400544 <+45>:	mov    $0x1,%esi
   0x0000000000400549 <+50>:	mov    $0x0,%edi
   0x000000000040054e <+55>:	callq  0x4004b6 <foo>

Learning inline keyword by example in C

C also has inline keyword, the sementic is not as same as C++. Inline Functions In C has a good explaination. In this blog, I am going to do dome exercises to make it more concrete.

static inline functions

This is very simple. Let's start with it.

// in foo.h
typedef void (*func_t)(const char * msg, void * f);
static inline void print_me(const char * msg, void * f)
{
   printf("%s: pointer is %p\n",msg,f);
}
// in main.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

extern void m1();
extern void m2();
int main(int argc, char *argv[])
{
    m1();
    m2();
    return 0;
}
// in a1.c
#include <stdio.h>
#include "foo.h"

void m1()
{
    foo("from a1", NULL);
}
16M// in a2.c
#include <stdio.h>
#include "foo.h"

void m2()
{
    foo("from a2", NULL);
}
% gcc -O3 -c -o a1.o a1.c
% gcc -O3 -c -o a2.o a2.c
% gcc -O3 -c -o main.o main.c
% gcc -o a.out a1.o a2.o main.o
% ./a.out
from a1: pointer is (nil)
from a1: pointer is (nil)
% objdump -d a1.o
a1.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <m1>:
   0:	31 d2                	xor    %edx,%edx
   2:	be 00 00 00 00       	mov    $0x0,%esi
   7:	bf 00 00 00 00       	mov    $0x0,%edi
   c:	31 c0                	xor    %eax,%eax
   e:	e9 00 00 00 00       	jmpq   13 <m1+0x13>
% nm a1.o
0000000000000000 T m1
                 U printf

With optimzation, we can see a1.o does not have local symbol foo defined, and there is no function call foo, i.e. it is inlined.

% gcc -O0 -c -o a1.o a1.c
% gcc -O0 -c -o a2.o a2.c
% gcc -O0 -c -o main.o main.c
% gcc -o a.out a1.o a2.o main.o
% ./a.out
from a1: pointer is (nil)
from a1: pointer is (nil)
% objdump -d a1.o

a1.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <foo>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
   c:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)
  10:	48 8b 55 f0          	mov    -0x10(%rbp),%rdx
  14:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  18:	48 89 c6             	mov    %rax,%rsi
  1b:	bf 00 00 00 00       	mov    $0x0,%edi
  20:	b8 00 00 00 00       	mov    $0x0,%eax
  25:	e8 00 00 00 00       	callq  2a <foo%0x2a>
  2a:	c9                   	leaveq
  2b:	c3                   	retq

000000000000002c <m1>:
  2c:	55                   	push   %rbp
  2d:	48 89 e5             	mov    %rsp,%rbp
  30:	be 00 00 00 00       	mov    $0x0,%esi
  35:	bf 00 00 00 00       	mov    $0x0,%edi
  3a:	e8 c1 ff ff ff       	callq  0 <foo>
  3f:	5d                   	pop    %rbp
  40:	c3                   	retq
% nm a1.o
0000000000000000 t foo
000000000000002c T m1
                 U printf

On the other hand, if we use -O0 to disable optimization, we can see foo is defined, and foo is invoked, i.e. it is not inlined.

how to force it to inline?

if we change the foo.h as following,

//in foo.h
...
static inline __attribute__((always_inline))
void foo(const char * msg, void * f)
...

__attribute__((always_inline)) force to inline the function, even when optimization is disabled.

sh build.sh
% gcc -O0 -c -o a1.o a1.c
% gcc -O0 -c -o a2.o a2.c
% gcc -O0 -c -o main.o main.c
% gcc -o a.out a1.o a2.o main.o
% ./a.out
from a1: pointer is (nil)
from a1: pointer is (nil)
% objdump -d a1.o

a1.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <m1>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	48 c7 45 f8 00 00 00 	movq   $0x0,-0x8(%rbp)
   f:	00
  10:	48 c7 45 f0 00 00 00 	movq   $0x0,-0x10(%rbp)
  17:	00
  18:	48 8b 55 f0          	mov    -0x10(%rbp),%rdx
  1c:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  20:	48 89 c6             	mov    %rax,%rsi
  23:	bf 00 00 00 00       	mov    $0x0,%edi
  28:	b8 00 00 00 00       	mov    $0x0,%eax
  2d:	e8 00 00 00 00       	callq  32 <m1+0x32>
  32:	c9                   	leaveq
  33:	c3                   	retq
% nm a1.o
0000000000000000 T m1
                 U printf

inline functions are not always inlined

// in a1.c
#include <stdio.h>
#include "foo.h"

void m1()
{
    foo("from a1", (void*) foo);
}
// in a2.c
#include <stdio.h>
#include "foo.h"

void m2()
{
    foo("from a2", (void*) foo);
}
sh build.sh
% gcc -O3 -c -o a1.o a1.c
% gcc -O3 -c -o a2.o a2.c
% gcc -O3 -c -o main.o main.c
% gcc -o a.out a1.o a2.o main.o
% ./a.out
from a1: pointer is 0x400530
from a1: pointer is 0x400570
% objdump -d a1.o

a1.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <foo>:
   0:	48 89 f2             	mov    %rsi,%rdx
   3:	31 c0                	xor    %eax,%eax
   5:	48 89 fe             	mov    %rdi,%rsi
   8:	bf 00 00 00 00       	mov    $0x0,%edi
   d:	e9 00 00 00 00       	jmpq   12 <foo+0x12>
  12:	66 66 66 66 66 2e 0f 	data16 data16 data16 data16 nopw %cs:0x0(%rax,%rax,1)
  19:	1f 84 00 00 00 00 00

0000000000000020 <m1>:
  20:	ba 00 00 00 00       	mov    $0x0,%edx
  25:	be 00 00 00 00       	mov    $0x0,%esi
  2a:	bf 00 00 00 00       	mov    $0x0,%edi
  2f:	31 c0                	xor    %eax,%eax
  31:	e9 00 00 00 00       	jmpq   36 <m1+0x16>
%  nm a1.o
0000000000000000 t foo
0000000000000020 T m1
                 U printf

we can see that the function foo is inlined, but the foo object code is emitted by compiler, because the address of the function foo is used.

Pitfall of static inline function

As same as other static function, every transform unit has its own implementation, so that foo is the local function. t in nm output indicates that foo is a private symbol.

The address of function foo is 0x400530 and 0x400570 for a1.o and a2.o respectively.

non-external inline function.

#pragma once
// in foo.h
inline __attribute__((always_inline))
void foo(const char * msg, void * f)
{
   printf("%s: pointer is %p\n",msg,f);
}
// in a1.c
#include <stdio.h>
#include "foo.h"

void m1()
{
    foo("from a1", (void*) NULL);
}
// in a2.c
#include <stdio.h>
#include "foo.h"

void m2()
{
    foo("from a1", (void*) NULL);
}
% gcc -O3 -c -o a1.o a1.c
% gcc -O3 -c -o a2.o a2.c
% gcc -O3 -c -o main.o main.c
% gcc -o a.out a1.o a2.o main.o
a2.o: In function `foo':
a2.c:(.text+0x0): multiple definition of `foo'
a1.o:a1.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
% nm a1.o
0000000000000000 T foo
0000000000000020 T m1
                 U printf
% nm a2.o
0000000000000000 T foo
0000000000000020 T m1
                 U printf

We can see both a1.o and a2.o emits function definition of foo, so that the linker complains that multile definition.

It means that the non-external inline function can only be used in one transform unit. This is rather rather limited use, it could be replace with static inline functions mentioned above.

extern inline functions

#pragma once
// in foo.h
extern inline
void foo(const char * msg, void * f)
{
   printf("%s: pointer is %p\n",msg,f);
}
// in a1.c
#include <stdio.h>
#include "foo.h"

void m1()
{
    foo("from a1", (void*) NULL);
}
// in a2.c
#include <stdio.h>
#include "foo.h"

void m2()
{
    foo("from a1", (void*) NULL);
}
% gcc -O3 -c -o a1.o a1.c
% gcc -O3 -c -o a2.o a2.c
% gcc -O3 -c -o main.o main.c
% gcc -o a.out a1.o a2.o main.o
% ./a.out
from a1: pointer is (nil)
from a1: pointer is (nil)
% objdump -d a1.o

a1.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <m1>:
   0:	31 d2                	xor    %edx,%edx
   2:	be 00 00 00 00       	mov    $0x0,%esi
   7:	bf 00 00 00 00       	mov    $0x0,%edi
   c:	31 c0                	xor    %eax,%eax
   e:	e9 00 00 00 00       	jmpq   13 <m1+0x13>
% nm a1.o
0000000000000000 T m1
                 U printf

We can see foo is inlined, because of -O3, and no code object is emitted.

But with the same source code but different compilation options, i.e. -O0, which disable inline optimization, we've got a linking error as below.

% gcc -O0 -c -o a1.o a1.c
% gcc -O0 -c -o a2.o a2.c
% gcc -O0 -c -o main.o main.c
% gcc -o a.out a1.o a2.o main.o
a1.o: In function `m1':
a1.c:(.text+0xf): undefined reference to `foo'
a2.o: In function `m2':
a2.c:(.text+0xf): undefined reference to `foo'
collect2: error: ld returned 1 exit status
% objdump -d a1.o

a1.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <m1>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	be 00 00 00 00       	mov    $0x0,%esi
   9:	bf 00 00 00 00       	mov    $0x0,%edi
   e:	e8 00 00 00 00       	callq  13 <m1+0x13>
  13:	5d                   	pop    %rbp
  14:	c3                   	retq
% nm a1.o
                 U foo
0000000000000000 T m1

Because foo is not inlined, and both a1.o and a2.o refer to and undefined reference to foo, but there is no code object for foo is emitted, so that there is the linking error, undefined reference.

Stand-alone object code is never emitted.

In order to fix the above error, we use non-external inline function in a1.c

// in a1.c
#include <stdio.h>
#include "foo.h"

void m1()
{
    foo("from a1", (void*) NULL);
}

inline void foo(const char * msg, void * f);
% gcc -O0 -c -o a1.o a1.c
% gcc -O0 -c -o a2.o a2.c
% gcc -O0 -c -o main.o main.c
% gcc -o a.out a1.o a2.o main.o
% ./a.out
from a1: pointer is (nil)
from a1: pointer is (nil)
% nm a1.o
0000000000000000 T foo
000000000000002c T m1
                 U printf

% nm a2.o
                 U foo
0000000000000000 T m2

We can see in a2.o, it is as same as before, foo is an external symbol. But in a1.c it is a public symbol, and foo should only be emitted by a1.o, otherwise it would have linking error multiple definition.

function address

If we change a1.c and a2.c as below, to print the address of function foo.

// in a1.c
#include <stdio.h>
#include "foo.h"

void m1()
{
    foo("from a1", (void*) foo);
}

inline void foo(const char * msg, void * f);

// in a2.c
#include <stdio.h>
#include "foo.h"

void m2()
{
    foo("from a1", (void*) foo);
}
% gcc -O0 -c -o a1.o a1.c
% gcc -O0 -c -o a2.o a2.c
% gcc -O0 -c -o main.o main.c
%  gcc -o a.out a1.o a2.o main.o
%  ./a.out
from a1: pointer is 0x400506
from a1: pointer is 0x400506
%  nm a1.o
0000000000000000 T foo
000000000000002c T m1
                 U printf
%  nm a2.o
                 U foo
0000000000000000 T m2

We see the address of function foo is unique, i.e. 0x400506.

what if inline functions have with difinitions?

// in a1.c
#include <stdio.h>
extern inline
void foo(const char * msg, void * f)
{
   printf("foo in a1.c %s: pointer is %p\n",msg,f);
}

void m1()
{
    foo("from a1", (void*) foo);
}

inline void foo(const char * msg, void * f);
// in a2.c
#include <stdio.h>
extern inline
void foo(const char * msg, void * f)
{
   printf("foo in a2.c %s: pointer is %p\n",msg,f);
}
void m2()
{
    foo("from a2", (void*) foo);
}
%  gcc -O0 -c -o a1.o a1.c
%  gcc -O0 -c -o a2.o a2.c
%  gcc -O0 -c -o main.o main.c
%  gcc -o a.out a1.o a2.o main.o
%  ./a.out
foo in a1.c from a1: pointer is 0x400506
foo in a1.c from a2: pointer is 0x400506
%  nm a1.o
0000000000000000 T foo
000000000000002c T m1
                 U printf
%  nm a2.o
                 U foo
0000000000000000 T m2

We notice that we didn't include the common definition of foo from foo.h, instead, a1.c and a2.c has its own definitions.

Because of -O0, the function is not inlined, so that only foo defined in a1.c is used.

But if we compile it with -O3.

%  gcc -O3 -c -o a1.o a1.c
%  gcc -O3 -c -o a2.o a2.c
%  gcc -O3 -c -o main.o main.c
%  gcc -o a.out a1.o a2.o main.o
%  ./a.out
foo in a1.c from a1: pointer is 0x400530
foo in a2.c from a2: pointer is 0x400530
%  nm a1.o
0000000000000000 T foo
0000000000000020 T m1
                 U printf
%  nm a2.o
                 U foo
0000000000000000 T m2
                 U printf

Because foo is inlined, a2.c uses the definition in a2.c, not the foo in a1.c.

Don't do it in practice.

But in practice it might happen. For example, you modify foo in foo.h, but you only compile a1.c and forgot to re-compile a2.c. If the function is inlined, a2.c still use the old definition of foo.

Different key binding in Emacs transient mark mode

Emacs transient mark mode gives you much of the standard selection-highlighting behavior of other editors, but key binding is not changed when mark is active, for example, many letter keys are bound to self-insert-command. This is quite different with other editors. In other editors, usually when mark is active and you input some texts, the new texts replace the selected text.

There is an easy way to temporarily change the key binding when mark is active and restore the original key bind automatically when mark is inactive.

(global-set-key (kbd "C-w") 'backward-kill-word)
(defconst wcy-transient-mode-map-alist
  `((mark-active
     ,@(let ((m (make-sparse-keymap)))
	 (define-key m (kbd "C-w") 'kill-region)
	 m))))
(add-to-list 'emulation-mode-map-alists
	     'wcy-transient-mode-map-alist)

The above example makes C-w have two different bindings. When mark is not active, it is backward-kill-word, similiar to the key binding under some shell, when mark is active, it binds to kill-region, following the emacs convention.

This example just illustrates the idea how to do it, maybe you might have your own preferred key bindings.

Emacs has a lot of key bindings, in such way, one key is able to bind to different emacs commands depending on whether the mark is active or not.

"hello world in C"

用 C 语言写一个 hello world 程序需要一下几个步骤:

  • 编辑、
  • 编译成为汇编
  • 汇编编译二进制目标代码
  • 链接成可执行文件

编辑

这一个步骤很简单,找一个你熟悉的编辑器,生成一个 hello_world.c, 如下。

extern int puts(const char * s);
int main(int argc, char *argv[])
{
    puts("hello world!");
    return 0;
}

这里我我没有使用常用的 #include <stdio.h>,我想强调的是 #include 不是必须的。

编译

在 linux 的命令行下,输入编译命令。如下。

% gcc -O3 -c -S -o hello_world.S  hello_world.c
  • -c 表示编译。
  • -S 表示输出汇编语言,默认直接输出二进制目标文件。
  • -o hello_world.S 表示指定输出文件名称。
  • -O3 表示强烈优化代码。这样生成的 asm 代码可读性更好。

我们如果察看 hello_world.S, 有如下结果

	.file	"hello_world.c"
	.section	.rodata.str1.1,"aMS",@progbits,1
.LC0:
	.string	"hello world!"
	.section	.text.unlikely,"ax",@progbits
.LCOLDB1:
	.section	.text.startup,"ax",@progbits
.LHOTB1:
	.p2align 4,,15
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	subq	$8, %rsp
	.cfi_def_cfa_offset 16
	movl	$.LC0, %edi
	call	puts
	xorl	%eax, %eax
	addq	$8, %rsp
	.cfi_def_cfa_offset 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.section	.text.unlikely
.LCOLDE1:
	.section	.text.startup
.LHOTE1:
	.ident	"GCC: (Debian 4.9.1-19) 4.9.1"
	.section	.note.GNU-stack,"",@progbits

生成二进制目标文件

% as -o hello_world.o hello_world.S

as 是一个汇编器, 把汇编代码转换成为二进制目标代码。

我们可以看看这个目标代码都有什么。

% nm hello_world.o
0000000000000000 T main
                 U puts

可以看到,我们的程序定义了连个符号,mainputs

  • T 表示在本目标文件中包含一大块内存的定义, 这一大块内存地址用 main 来表示。这一块内存里面存储的就是main 函数的机器指令。

  • U 表示本目标文件没有引用了一个符号 puts 但是不知道这个符号定义在什么地方。

我们可以看一下这个目标文件的内容。

% objdump -d hello_world.o

hello_world.o:     file format elf64-x86-64


Disassembly of section .text.startup:

0000000000000000 <main>:
   0:	48 83 ec 08          	sub    $0x8,%rsp
   4:	bf 00 00 00 00       	mov    $0x0,%edi
   9:	e8 00 00 00 00       	callq  e <main+0xe>
   e:	31 c0                	xor    %eax,%eax
  10:	48 83 c4 08          	add    $0x8,%rsp
  14:	c3                   	retq

我们可以看到,main 对应的地址是 0 ,其实这个地址会变化,这就是链接 的作用。下面就是机器指令。c3 机器指令 retq

在 x64 中, 我们调用函数的时候,如果参数少于 6 个,那么参数是由寄存器 传递,即, RDI, RSI, RCX, RDX, R8, 和 R9 。多余 6 个就用 stack 传递。

我们看到 %edi 的值是 0mov $0x0, %edi ,不是我们输入的 "helloworld"的地址。调用函数 puts 也变成了 callq e。这是因为还没 有链接过。一个目标文件是不能知道这些地址是什么。

链接

一个可执行程序必须有入口,通常我们说是 main 函数。其实这个是可以指定的。ld 输出的可执行文件,都有一个入口函数。这个入口函数默认不是main 而是 _start 。 这个 _start 函数定一个在 crt1.o 的文件里 面。puts 定义在 libc.a 里面。所以我们的链接命令是。

% ld -o hello_world \
   -dynamic-linker "/lib64/ld-linux-x86-64.so.2"\
    hello_world.o \
    "/usr/lib/x86_64-linux-gnu/crt1.o"\
    "/usr/lib/x86_64-linux-gnu/crti.o"\
    "/usr/lib/gcc/x86_64-linux-gnu/4.9/crtbegin.o"\
    "-L/usr/lib/gcc/x86_64-linux-gnu/4.9" \
    -lc  \
    "/usr/lib/gcc/x86_64-linux-gnu/4.9/crtend.o"\
    "/usr/lib/gcc/x86_64-linux-gnu/4.9/../../../x86_64-linux-gnu/crtn.o"
  • -o hello_world 指明输出可执行文件的名称。
  • hello_world.o 我们刚刚生成的目标文件。
  • -dynamic-linker 指定 ld-linux-x86-64.so.2 是 loader ,否则无法加载可执行文件。
  • .../crt1.o , .../crti.ocrtbegin.o, crtn.o, crtend.o 就 是负责处理程序开始和结束的一些内容。例如 atexit 之类的。定义 _start
  • -lc 表示链接标准 c 的库函数,puts 就定义在里面。

我们看一下 hello_world 的内容。

% objdump -d  hello_world

hello_world:     file format elf64-x86-64


Disassembly of section .init:
....
0000000000400370 <puts@plt>:
  400370:	ff 25 92 03 20 00    	jmpq   *0x200392(%rip)        # 600708 <_GLOBAL_OFFSET_TABLE_+0x18>
  400376:	68 00 00 00 00       	pushq  $0x0
  40037b:	e9 e0 ff ff ff       	jmpq   400360 <_init+0x18>

Disassembly of section .text:

00000000004003a0 <main>:
  4003a0:	48 83 ec 08          	sub    $0x8,%rsp
  4003a4:	bf 58 04 40 00       	mov    $0x400458,%edi
  4003a9:	e8 c2 ff ff ff       	callq  400370 <puts@plt>
  4003ae:	31 c0                	xor    %eax,%eax
  4003b0:	48 83 c4 08          	add    $0x8,%rsp
  4003b4:	c3                   	retq

00000000004003b5 <_start>:
  4003b5:	31 ed                	xor    %ebp,%ebp
  4003b7:	49 89 d1             	mov    %rdx,%r9
  4003ba:	5e                   	pop    %rsi
  4003bb:	48 89 e2             	mov    %rsp,%rdx
  4003be:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
  4003c2:	50                   	push   %rax
  4003c3:	54                   	push   %rsp
  4003c4:	49 c7 c0 50 04 40 00 	mov    $0x400450,%r8
  4003cb:	48 c7 c1 e0 03 40 00 	mov    $0x4003e0,%rcx
  4003d2:	48 c7 c7 a0 03 40 00 	mov    $0x4003a0,%rdi
  4003d9:	e8 a2 ff ff ff       	callq  400380 <__libc_start_main@plt>
  4003de:	f4                   	hlt
  4003df:	90                   	nop
  ...

这个输出太长,我截断了一些。

我们可以看到 _start 函数的定义,看到 puts 函数的定义,puts 在地 址 0x400470。看到 main 函数数的定义,地址在 0x4003a0, 看调用 puts 的语句变成了。

4003a9:	e8 c2 ff ff ff       	callq  400370 <puts@plt>

4003a9 表示这条机器指令的绝对位置。callq e 也变成了 callq 400370,即 puts 的地址。

4003a4:	bf 58 04 40 00       	mov    $0x400458,%edi

"hello world" 的地址也不是 0 了,而是 0x400458

这就是链接器的作用,原来不确定的地址,例如 main, "hello world", puts都变成了活生生的地址了。

运行

% ./hello_world
hello world!

简化过程

我们不必每一次都这么麻烦,指定这么多的细节命令,gcc 可以一次从头干到 尾。实际上,很少有人直接这么一步一步地做,都是直接调用 gcc 一步到位。

% gcc -o hello_world hello_world.c
% ./hello_world
hello world!

如果有多个文件,常用的作法是

% gcc -c -o hello_world.o hello_world.c
% gcc -o hello_world hello_world.o
% ./hello_world
hello world!