Recording

Commit volatile memory to persistent append-only log

0%

C 语言中具有静态生存周期的变量的初始化器必须是常量表达式。当初始化器不是常量表达式时,gcc 和 clang 的报错信息不尽相同。

gcc 的报错信息是 error: initializer element is not constant clang 的报错信息是 error: initializer element is not a compile-time constant

clang 用的是“编译时常量”(compile-time constant),实际上 C99 标准里没有“编译时常量”这个概念,只有“常量表达式”。

A constant expression can be evaluated during translation rather than runtime, and accordingly may be used in any place that a constant may be.

C99 规定了初始化器中可用的常量表达式:

More latitude is permitted for constant expressions in initializers. Such a constant expression shall be, or evaluate to, one of the following:

  • — an arithmetic constant expression,
  • — a null pointer constant,
  • — an address constant, or
  • — an address constant for an object type plus or minus an integer constant expression.

An address constant is a null pointer, a pointer to an lvalue designating an object of static storage duration, or a pointer to a function designator; it shall be created explicitly using the unary & operator or an integer constant cast to pointer type, or implicitly by the use of an expression of array or function type. The array-subscript [] and member-access . and -> operators, the address & and indirection * unary operators, and pointer casts may be used in the creation of an address constant, but the value of an object shall not be accessed by use of these operators.

也就是说,你可以在初始化器中使用变量、变量的成员和函数的地址,也可以在地址的基础上加上个偏移量。

“address constant” 不能算作 “compile-time constant”。 尽管编译器可能在生成 ELF 文件之时,就决定了变量的虚拟地址,但在作为共享库 (shared library) 被加载时或其他情况下,这些变量的虚拟地址最终还需要加载器 (loader) 的调整。对于这些变量地址的引用,需要加载器的重定位功能。

c99 标准对于 “address constant” 的运算只规定了可以加减整数常量表达式。

1
2
3
#include <stdint.h>
#include <stdlib.h>
uintptr_t addr = (uintptr_t)malloc + (uintptr_t)free + 0x2345;
Read more »

学习一门新的语言时,我觉得最重要的是学习该语言的惯例用法(idioms),即其编程范式。

很遗憾的是 C++ 是一门日渐进化并且日趋复杂的语言,是一个具有多重编程范型的语言,被视为一个语言联邦1

即使这样,我认为在日常 C++ 编程时,也应该选择一个相对固定的编程模型,实验并改进之。

在用 C++ 编程时,应该尽量保持谦卑、进取与怀疑。

下面主要记录一下我写 C++ 的不长的时间里的一些经验。

避免编译时依赖。

C++ 没有现代的 package, import 机制,而是使用了继承自 C 的 #include 头文件包含机制。C++ 的复杂和 C 的编译模型导致了 C++ 项目冗长的编译时间。在其他语言中 import/require 可以解决的问题,在 C++ 中必须要程序员自己付出努力。[2]

隐式 (implicit) 的编译依赖不像头文件依赖会导致编译期的错误,却会在运行期招致不可预估的致命错误。比如,当你在编译单元 CompilationUnitA 中 p = new(CompilationUnitB::ClassB) 时,你已经埋下了一颗地雷。以后当你改变 CompilationUnitB::ClassB 的成员而导致其大小 (size) 变化时,你编译 CompilationUnitB ,链接 CompilationUnitA ,运行,然后不知何时那颗地雷就会炸掉。我觉得就 new 的编译期行为,C++ 能做的更好,也应该做的更好。好吧,永远不要跨编译单元调用 new

虚函数调用、跨编译单元的栈上变量/全局变量同样是隐式的编译依赖。

跨编译单元(.so, .dll, .lib)时,区分接口与实现;同一编译单元内,不过度区分接口与实现。

Read more »

封装是 C++ 面向对象三大特性之一。C++ 通过将数据和对数据操作的函数绑定在一个类里来表达封装。从字面意义和 C++ 的历史(C with class)来看,封装对于 C++ 来说是根本的。

《代码大全2》6.2节 中提到了一个良好的封装应该“避免把私有的实现细节放入类的接口中”,并介绍了 《Effective C++》 中提到的 pImpl 惯用技法。

在我看来 C++ 中类的实现方式明显无法很好的表达封装,只是粗暴的将函数及其操作的数据绑在一起,而忽视了可见性的控制。C++ 对运行时性能苛求可能是其罪魁祸首。

实际上我认为封装从根本上就是一个错误的观点,数据和对数据操作的函数从来就不是一个整体。

好吧,本文要说只是另一种定义 C++ 类的方式,类似 Objective-C 类的 interface/implementation 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// foo_interface.hpp
#include <boost/noncopyable.hpp>

namespace foo {

class Interface : boost::noncopyable {
public:
int Bar();
};

}

// foo.hpp
#include "foo_interface.hpp"

#include <boost/shared_ptr.hpp>

namespace foo {

typedef boost::shared_ptr<Interface> Reference;

Reference New();

}

// foo.cpp
#include "foo.hpp"

namespace foo {

class implementation : public Interface {
public:
int Bar() {
// ...
return bar0();
}

implementation() {
// ...
}

private:
int priv_data0;
// ...

int bar0() {
// ...
}

};

int Interface::Bar() {
BOOST_AUTO(impl, static_cast<implementation>(this));
return impl->Bar();
}

Reference New() {
return Reference(new implementation());
}

}

foo::Interface 中只包含 foo 对外提供的接口函数,没有数据成员。

Read more »

先引用一下,0mq manual 的话:
0MQ ensures atomic delivery of messages; peers shall receive either all message parts of a message or none at all.

在 0mq 中最终数据的发送和接收是由 encoder_t 和 decoder_t 负责包装。他们都是由 io 线程调用,从应用发送 zmq_msg_t 的管道中取得数据。

1
typedef ypipe_t < zmq_msg_t, message_pipe_granularity > pipe_t;
1
2
3
4
5
6
//  Lock-free queue implementation.
// Only a single thread can read from the pipe at any specific moment.
// Only a single thread can write to the pipe at any specific moment.
// T is the type of the object in the queue.
// N is granularity of the pipe, i.e. how many items are needed to
// perform next memory allocation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//  Write an item to the pipe.  Don't flush it yet. If incomplete is
// set to true the item is assumed to be continued by items
// subsequently written to the pipe. Incomplete items are never
// flushed down the stream.
inline void write (const T &value_, bool incomplete_)
{
// Place the value to the queue, add new terminator element.
queue.back () = value_;
queue.push ();

// Move the "flush up to here" poiter.
if (!incomplete_)
f = &queue.back ();
}

write 函数的 incomplete_ 参数由外部的包装函数根据 ZMQ_SNDMORE 标志决定。只有当 multipart message 的最后一部分写入管道时,f 才会更新。当 encoder_t 读取 zmq_msg_t ,准备发送时,他不会读到一个未全部写完的 multipart message。decoder_t 从网络上接收数据后,在未读到最好一个部分时不会更新 f。

其实 ypipe_t 是个挺有意思的两端可以同时允许一个线程读、一个线程写的 lock-free 的数据结构实现。

没什么好写的啊,看代码就OVER了。凑数。

Read more »

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Virtual interface to be exposed by object that want to be notified
// about events on file descriptors.

struct i_poll_events
{
virtual ~i_poll_events () {}

// Called by I/O thread when file descriptor is ready for reading.
virtual void in_event () = 0;

// Called by I/O thread when file descriptor is ready for writing.
virtual void out_event () = 0;

// Called when timer expires.
virtual void timer_event (int id_) = 0;
};

0mq 中实现(或部分实现)了这些接口的类包括: io_thread_t, reaper_t, zmq_connect_t, zmq_listener_t, zmq_engine_t等。

Read more »