核桃的炼金工坊

程序员 / WOWer(无家可归) / 日语在学 / 退役竞赛废物

2023 韩国游记

巨疲惫的黄金周,今年这个十一的主题是流浪!先是个 7 天的首尔+济州岛! 话说出来之前,我还在说着我刚刚用发展中国家的身份得到了 Readwise 50% 的优惠,这不马上就要去发达国家感受一下发达了。哈哈哈。 首尔 首尔是盖在了山上嘛!怎么都是楼梯! ">首尔是盖在了山上嘛!怎么都是楼梯! 1 首尔真的是一个感觉很卷的地方,空气都是咖啡味的。而且喝到的所有咖啡店基本都是中深烘的豆子,其实自己是更偏好一点烘焙度浅一些的豆子的,可以更突出果香和酸质。但是韩国的咖啡有一个贼大的好处,量大够冰!我们戏称他们喝咖啡其实是单纯的为了里面的咖啡因罢了,他们的咖啡不是生活而是工作。 说起来真的挺离谱的,他们应该是很卷的,不管是地铁还是咖啡店都会开到非常晚,总感觉他们心里应该有很多很难受的地方。直到半夜还在咖啡店大口喝着冰美式聊天的人们,如果偶尔出现我还可以理解,但是如果每天都是这样的话,应该有挺多压力的。 2 第一天的第一顿小泡面 ">第一天的第一顿小泡面! 酒店的对面有应该算是饭店一条街的地方,一到晚上就聚满了人。里面有第一天吃的好吃的泡面、拌饭,也有后来经常吃的牛骨汤、脆骨、鸡爪什么的。但是有些不同的是,这里好像总是有很多的年轻人,稍微上了年纪的人非常少的样子。年轻人们即使是到了晚上,出现在像大排档一样的小饭馆里,也是衣服穿的笔挺的,裤子都像是刚熨过的样子,很少看到一天的风尘。 是挺好看的,但是应该也挺累的。 3 到过一个大学的边上,好像是弘益大学,看 wiki 上讲应该是个很好的大学,主要是艺术领域。怪不得周围的商圈还有画画的。hhh Uncle Painting ">Uncle Painting 还是维持了在首尔 2 步一个咖啡店的密度,不过这里会有一些感觉不像是连锁品牌的咖啡。像是国内的自己开的小咖啡店什么的,店里还做司康,不出意外还是深烘的豆子,但是配上甜甜的奶油,味道倒还是不错。 oats! ">oats 封面就是用的这张图,咖啡是好喝的,店也好看的,店员小姐姐也酷酷的。哈哈哈 4 参鸡汤店 ">参鸡汤店 去拍韩服的时候路过的一家参鸡汤店,说实话这是我第一次尝试的品类,味道还不错。据说是很补,但是谁知道呢。 拍韩服的时候的摄影师很健谈,我们各自用蹩脚的英语交流,他看看我挂在脖子上的相机,还问我是不是专业的。我只能说 just hobby。相似的衣服,差不多的宫殿,完全不相干的文字,这就是韩国传统部分给我的印象。 路上还在讨论说现在的韩语是不是一个更好的简化。从文字普及的角度上来讲,应该是的。从表意来讲,应该是退步了一些的。纯表音的文字,怎么表达同音的不同意呢。这不就有点可惜了。 济州岛 济州岛基本就是一个很躺的地方了,在济州岛的几天基本都在海边的咖啡店里摆烂,喝着咖啡,和同行来的朋友聊聊天,看看落日,一天就很快这么过去了。要说有什么不一样的,倒是拍了不少好看的照片。

2023/10/4
articleCard.readMore

C++23: Flat Containers

C++23 的周期里又补充了四个非常重要的容器,这应该是继 C++11 加入 unordered_{map|set} 以来, 终于再次向 STL 里增加的容器。这四个容器是其实就是 map/set/multimap/multiset 的 flat 版本。 flat_map flat_set flat_multimap flat_multiset 1 Specialties 这组新的容器可以在合适的场景下替换其对应的容器(当然也包括了他们的 unordered_ 的版本), 和同功能的其他容器相比: 更慢的插入和删除,因为插入和删除都会导致元素的移动,像 vector 的 insert 一样 插入和删除都会造成旧迭代器的失效,因为不论是插入还是删除,都会伴随着大部分元素的移动,保存的旧迭代器很难不失效 元素类型必须是可移动(moveable)或者可拷贝的(copyable) 异常安全变得更弱了,像 vector 一样构造函数的异常没法保证后面的元素可以成功构造 那上面付出的代价,我们获得了什么? 更快的遍历,在一个简单数组上的一次后继的时间复杂度是 $O(1)$ 而对于 map 这个时间是$O(log_2n)$ 得益于 vector 的实现,现在我们的迭代器是 random access 的了 更小的空间复杂度,对于 map 不用保存节点的额外信息了,对于 unordered_map 不会有空的 slot 更好的缓存性能(因为存储是连续的) 更快的查找,虽然数量级上都还是 $O(log_2n)$ 但是常数上小了很多 Container Ordered Insert Erase Find iter++ map/set Yes $O(log_2n)$ $O(log_2n)$ $O(log_2n)$ $O(log_2n)$ unordered_{map/set} No $O(1)$ $O(1)$ $O(1)$ $O(1)$ flat_{map/set} Yes $O(n)$ $O(n)$ $O(log_2n)$, but smaller constant $O(1)$ 所以它应该更适合存那些构造出来基本就不会改的数据,获得更好的遍历和查找性能。 2 References C++23: flat_map, flat_set, et al. A Standard flatmap A Standard flatset

2023/9/21
articleCard.readMore

Deducing This

1 What is deducing this? A new way of declaring non-static member functions that will allow for deducing the type and value category of the class instance parameter while still being invocable with regular member function syntax. 简单的说,就是让非静态成员函数的 this 参数成为一个显式参数。而不破坏以前的调用成员函数的语法。 2 Semantics The new syntax is to use an explicit this-annotated parameter. 一个非静态成员函数可以把它的第一个参数成为一个显式对象参数 (explicit object parameter),用一个关键字前缀 this 来表示。 这个参数可以像其他参数一样,使用 cv- 和 ref- 限定符。那么当然也可以用模板。 cpp struct Foo { void foo(this const Foo&); void foo(this volatile Foo&&); template<typename Self> void bar(this Self self); void barbar(this auto self); }; 非静态成员函数,隐式决定对象类型和显式对象参数类型的表示: Implicit object Explicit Object Parameter void foo() &; void foo(this X&); void foo() const &; void foo(this const X&); void foo() &&; void foo(this X&&); 在调用一个这样的成员函数的时候,这个对象会作为第一个参数,其他参数会依次推后一个。 ( 比如括号里的第一个参数,实际是声明里的第二个参数。 ) 2.1 Function Type 考虑下面两个不同的函数声明,他们的类型分别为: cpp struct Y { int f(int, int) const&; int g(this Y const&, int, int); }; &Y::f is int(Y::*)(int, int) const& &Y::g is int(*)(const Y&, int, int) 也就是说,带有 explicit object parameter 的函数可以理解成被视作静态成员函数的。 所以静态成员函数也不能有 explicit object parameter。 实际上带有 explicit object parameter 不同与非静态成员函数和静态成员函数, 是另一种成员函数类型。 They’re like semi-static member functions. We’ll call them explicit object member functions due to them having an explicit object parameter. 非静态 EOMF 静态 Requires/uses an object argument Yes Yes No 隐式声明了 this Yes No No 可以声明重载运算符 Yes Yes No &C::f 的类型 成员函数指针 函数指针 函数指针 虚函数? Yes Maybe No auto f = x.f; No No Yes 名字可以退化成函数指针 No No Yes 2.2 Candidate Functions 有了 Deducing this 之后,新加入了带有 explicit object parameter 的 candidates。 The only change in how we look up candidate functions is in the case of an explicit object parameter, where the argument list is shifted by one. The first listed parameter is bound to the object argument, and the second listed parameter corresponds to the first argument of the call expression. 由于带 explicit object parameter 的函数会被视作静态函数,并且在参与重载决议的时候, 参数被视作从第二个参数开始。所以下面这两个函数定义是冲突的: cpp static void foo(); void foo(this X const&); 3 Use cases 3.1 避免重复 类似 std::optional<T> 的例子: 3.1.1 Before cpp template<typename T> struct Optional { auto value() const & { if (has_value()) { return value_; } throw std::bad_optional_access(); } auto value() const&& { if (has_value()) { return std::move(value_); } throw std::bad_optional_access(); } auto value() && { if (has_value()) { return std::move(value_); } throw std::bad_optional_access(); } auto value() & { if (has_value()) { return value_; } throw std::bad_optional_access(); } }; 3.1.2 After deducing this cpp template<typename T> struct Optional { auto value(this auto&& self) { if (self.has_value()) { return std::forward_like<decltype(self)>(self.value_); } throw std::bad_optional_access(); } }; 3.2 Recursive Lambdas lambda 一直都有个问题是递归比较困难,因为没法拿到 lambda 函数自己这个对象。 所以一般都是用些 trick 的方法。要不就是转成一个 std::function 的对象存起来, 要不就是要在每次调用的时候,都把自己作为一个参数传给自己,像这样: 3.2.1 Before cpp std::function<int(int)> fib; fib = [&fib](int i) { if (i < 2) return 1; return fib(i - 1) + fib(i - 2); }; auto fib2 = [](auto& fib, int i) { if (i < 2) return 1; return fib(i - 1) + fib(i - 2); }; std::cout << fib2(fib2, 10); 3.2.2 After deducing this cpp auto fib = [](this auto self, int i) { if (i < 2) return 1; return self(i - 1) + self(i - 2); }; Update 2023.11.8 这个递归甚至可以被用在 std::visit 里面: cpp struct Leaf {}; struct Node; using Tree = std::variant<Leaf, Node*>; struct Node { Tree left, right; }; template <typename... Ts> struct overload : Ts... { using Ts::operator()...; } int countLeaves(const Tree& tree) { return std::visit(overload{ [] (const Leaf&) { return 1; }, [] (this const auto& self, const Node* node) -> int { return visit(self, node->left) + visit(self, node->right); } }, tree); } 3.3 CRTP without CRT CRTP 一个重要的部分,就是要在基类里得到子类的类型,这个一般都是把基类变成一个模板类, 然后在继承的时候把子类类型传进去得到的。有 Deducing this 之后,explicit object member function 的 explicit object parameter 是模板的话,就可以在调用的时候得到实际子类的类型。 3.3.1 Before cpp template<typename D> struct CRTP { void foo() { static_cast<D*>(this)->fooImpl(); } }; 3.3.2 After deducing this cpp struct CRTP { void foo(this auto& self) { self.fooImpl(); } }; 3.3.3 By-value member functions: Move-into-parameter chaining cpp struct Builder { Builder name(this Builder self, auto value) { self.name_ = std::move(value); return self; } Builder password(this Builder self, auto value) { self.password_ = std::move(value); return self; } }; 3.3.4 By-value member function: Performance improvement 对于像 std::string_view 这类的小对象,传值是个比传引用更快,所以应该更多传值而不是传引用, 对于这类的对象。但是在以前一个函数调用会把 *this 作为一个参数传着。对于这种对象, 把 *this 变成 Self 显然是个很大的性能优化。 cpp struct StringView { auto length(this Stringview self) { return self.length; } }; 4 References: C++ Draft: Duducing this

2023/9/17
articleCard.readMore

Stateful Metaprogramming

1 Stateful Metaprogramming A C++ trick is known as “stateful metaprogramming”, in which one manipulates friend functions and template instantiation to introduce a utilizable state into the compilation. cpp constexpr bool func() { /* something */ } constexpr bool a = func(); constexpr bool b = func(); // Think this assertion always fails? With stateful metaprogramming, think again. static_assert(a != b); Maybe ill-formed? or UB? 1.1 Explicit Template Instantiation cpp auto flag(auto); template<bool val> struct flag_setter { friend auto flag(auto) {} }; int main() { flag(1); // 1, error flag_setter<true>{}; flag(1); // 2, ok } During the template class flag_setter instantiation, a template class specialization has been added to the global scope. The specialization defines the function flag() (by friend function). So the second invocation success. 1.2 Constant Expression Switch cpp auto flag(auto); template<bool val> struct flag_setter { friend auto flag(auto) { } bool value = false; }; template<auto arg = 0, auto condition = requires { flag(arg); }> consteval auto value() { if constexpr (!condition) { return flag_setter<condition> {}.value; } return condition; } int main() { constexpr auto a = value(); // 1: false constexpr auto b = value(); // 2: true static_assert(a != b); } At // 1 the template class flag_setter has not been instantiated. We got false. After the first invocation, the template class flag_setter has been instantiated. The function flag() is defined. We got true. 1.3 Compile-time Counting cpp template<std::size_t N> struct reader { friend auto counted_flag(reader<N>); }; template<std::size_t N> struct setter { friend auto counted_flag(reader<N>) {} std::size_t value = N; }; template<auto N = 0, auto tag = []{}, bool condition = requires(reader<N> red){ counted_flag(red); }> constexpr auto next() { if constexpr (!condition) { constexpr setter<N> s; return s.value; } else { return next<N + 1>(); } } int main() { constexpr auto a = next(); constexpr auto b = next(); constexpr auto c = next(); static_assert(a == 0 && b == 1 && c == 2); } 2 References: Is stateful metaprogramming ill-formed (yet)? Revisiting Stateful Metaprogramming in C++20 Compiler Explorer

2023/8/1
articleCard.readMore

推し、燃ゆ

(全部的内容都是零碎的,没头没尾的,写给自己的,当下的想法,没想是个文章,就这样吧) 仅仅是起床,床单就会皱起来;仅仅是活着,人也会皱起来。和其他人说话需要绷紧理论上的肉,身体脏了需要泡澡,指甲长长了需要剪掉。最低限度的活着,也并非绞尽力气就一定能做到。我总是在完成最低限度之前,意识和身体就断联了。 “推し”是“押す”的名词形式,是偶像发展起来才出的新用法。指“本命”,“为偶像应援”。 “燃ゆ”是“燃える”的古语形式,有“焚烧”、“死亡”的意思。 整部小说细碎的情节,只能让我在情感上感受到前后段的一点点粘连。从高中退学、不和睦的家庭、失去的经济来源构成了在偶像出事之后的破烂生活。她大抵是努力过的吧。 啊,是彼德·潘。毫无疑问,他就是那天从我头顶飞过的男孩。 所以是什么让她炽热的沉浸在“推し”这件事情上的呢。是那天在头顶飞过的男孩,是在这个过程中才能感觉到真实存在的自己吧。也应该是那句,“才不想成为大人”的共鸣。 怀抱相同烦恼的人影,凭依他的身体站在了眼前。我与他相连,与他对面的众人相连。 书里的旁人会质问 “年轻倒是没问题,但必须面对现实里的男人啊,否则很容易误入歧途。” 成年人的傲慢在这一刻,就只要这一句,就猛的朝我扑过来。他大抵只是想要一杯浓点的酒吧,在没被满足之后用这样的方式发泄着他的不快。他们应该是认为,偶像和饭的关系是畸形的?不和他们一样的?不能理解的? 其实在里面,偶像是逃避。是明里在痛苦、在软弱、在不知所措的时候逃避的对象。要不怎么会被一句“才不想成为大人”击中呢。 我看了他的演唱会,看了他的电影,看了他的综艺。虽然声音和体型都不同了,但眼瞳深处不经意间流露出的锐利仍然与年幼的他如出一辙。那样的眼神,会让我也不由自主地想怒视些什么。既不正面也不负面的巨大能量从身体深处喷涌而出,提醒我活着这件事。 偶像是逃避的港湾,后来也变成了她的脊梁。 是她抱在怀里的衣物对我造成了无法再逃避的伤害。我堆放在房间里的大量档案、写真、CD,那些我用尽全力收集来的东西,都不如一件衬衫、一双袜子更能真切地描绘一个人的现在。偶像已经退圈,他的将来会有其他人在身旁注视着。这才是我要面对的现实。 乌鸦在啼叫。我环视着整个房间,光从回廊、窗户投射进来,将房间照的亮亮的。不仅是中心,全部的一切都会成为我活着的结果。骨头是我,血肉也是我。我回想起抛出棉签盒之前的场景。一直忘记收的杯子、残留着汤汁的盖饭、遥控器。视线扫过,最终,我还是选择收拾起来最轻松的棉签盒。想笑的冲动像气泡一样涌了上来,扑哧一声,又消失了。 可最终生活还是要继续的,一片狼藉的生活还是要继续的。在所有的东西里,选了一个最容易的棉签盒用力的扔出去,最终也只能自己捡回来。然后收拾好屋子里的长了白色霉菌的饭团、空可乐瓶。也捡起来了自己狼藉个生活,既然不适合两足行走,那就趴在地上前进吧。

2022/9/12
articleCard.readMore

Customization Point Object

1 解决什么问题? 标题是要聊一下 C++20 带来的一些新的很有意思的新机制,或者说是新轮子。 用来解决库函数或者一些通用函数定制用户类型的行为的抽象。 比如,我现在要实现一个通用的算法: cpp template<typename T> void foo(T& vec); 我可能需要我的参数 vec 的类型 T。可以拿到他的两个对应迭代器和大小。(这里只是举个例子) 在这个时候,就是我的通用函数需要定制用户类型的行为。或者说,需要用户的类型提供我需要的能力。 对于这样的每一个对用户能力的要求,被称为一个 定制点。 2 现在都有哪些方案? 2.1 继承 第一个办法显然是继承嘛,考虑这个例子: c++ class ConnectionBase { void on_buffer_received(std::span<char> buffer) { if (handle_buffer(buffer)) { // ... } } protected: virtual bool handle_buffer(std::span<char> buffer) = 0; }; class TlsConnection : public ConnectionBase { protected: bool handle_buffer(std::span<char> buffer) { // ssl... } }; 基类的纯虚函数 ConnectionBase::handle_buffer(std::span<char> buffer) 就是一个定制点。它在这里需要用户类型,也就是它的子类定义如何处理收到的 buffer 的行为。 2.2 CRTP(Curiously Recurring Template Pattern) 还是考虑上面那个例子: c++ template<typename D> class ConnectionBase { void on_buffer_received(std::span<char> buffer) { if (static_cast<D*>(this)->handle_buffer(buffer)) { // ... } } }; class TlsConnection : public ConnectionBase<TlsConnection> { bool handle_buffer(std::span<char> buffer) { // ssl... } }; 这样基类可以直接访问子类的功能和实现,而且绕过了依赖虚表实现的多态调用有更好的性能。 缺点是在这样的情况下,定制点变的非常不明显,他可以不依靠任何的在基类中的声明来表达子类需要实现哪些定制点。 2.3 ADL(Argument-Dependent Lookup) 依旧是上面那个例子: c++ namespace tls { class TlsConnection {}; bool handle_buffer(TlsConnection* conn, std::span<char> buffer); // #1 } namespace tcp { class TcpConnection {}; bool handle_buffer(TcpConnection* conn, std::span<char> buffer); // #2 } tls::TlsConnection* tls; tcp::TcpConnection* tcp; handle_buffer(tls); // #1 handle_buffer(tcp); // #2 在这种方案下,通过在对应参数的 namespace 来实现对应的定制功能,然后通过 ADL 来找到正确的实现。 在这样的方案下带来的一个问题就是,对于这个 handle_buffer,我们使用带 namespace 和不带 namespace 的版本可能会带来不一样的结果。 3 新的方案是什么样的? 在 C++20 里,其实 ranges 也面临了类似的问题,需要兼容各种类型的容器就要为每个类型的容器定制对应的功能。ranges 给出的方案就是 CPO,customization point object。 A customization point object is a function object with a literal class type that interacts with program-defined types while enforcing semantic requirements on that interaction. CPO 有很好的泛型的兼容性,而且也可以弥补上面 ADL 所提到的问题。 考虑一个取一个任意类型容器的 begin iterator 的问题: c++ namespace _Begin { class _Cpo { enum class _St { _None, _Array, _Member, _Non_member }; template <class _Ty> static _CONSTEVAL _Choice_t<_St> _Choose() noexcept { if constexpr (is_array_v<remove_reference_t<_Ty>>) { return {_St::_Array, true}; } else if constexpr (_Has_member<_Ty>) { return {_St::_Member, noexcept(_Fake_decay_copy(_STD declval<_Ty>().begin()))}; } else if constexpr (_Has_ADL<_Ty>) { return {_St::_Non_member, noexcept(_Fake_decay_copy(begin(_STD declval<_Ty>())))}; } else { return {_St::_None}; } } template <class _Ty> static constexpr _Choice_t<_St> _Choice = _Choose<_Ty>(); public: template <_Should_range_access _Ty> requires (_Choice<_Ty&>._Strategy != _St::_None) _NODISCARD constexpr auto operator()(_Ty&& _Val) const { constexpr _St _Strat = _Choice<_Ty&>._Strategy; if constexpr (_Strat == _St::_Array) { return _Val; } else if constexpr (_Strat == _St::_Member) { return _Val.begin(); // #1: via member function } else if constexpr (_Strat == _St::_Non_member) { return begin(_Val); // #2: via ADL } else { static_assert(_Always_false<_Ty>, "Should be unreachable"); } } }; } inline namespace _Cpos { inline constexpr _Begin::_Cpo begin; } 这个是 ranges 库里面的 begin 的实现,我只留下了为了说明问题的关键的部分。在这个实现里,用 if constexpr 处理了类型选择的大部分的逻辑。对于对一个容器类型求 begin 的操作被分成了三种情况,数组、成员函数、非成员函数。数组是 builtin 的类型就不聊了嘛;成员函数的调用类似上面的 CRTP 的用法,因为通过模板是明确拿到当前容器的类型的,可以直接调用对应自定义类型的成员;非成员函数的调用因为是直接用的 begin 这个名字,所以这里使用了上面的 ADL 的方式来找到对应容器自定义类型正确的处理函数。 为什么是一个 function object 而不是一个模板函数,是因为 function object 在这个情况下是不会被应用 ADL 的,也就是说不会被 ADL 找到的,避免了在 #2 那个地方的 ADL 递归调用的问题。 references: C++ Draft: customization point object Cppreference: ranges Niebloids and Customization Point Objects

2022/9/6
articleCard.readMore

2020 总结

0.1 0x01 平时其实都没有写类似年终总结这类东西的习惯的,但是今年就想了想,还是想搞点什么东西。 或者是记下来我这个和平时也没啥区别的一年? 0.2 0x02 本质上今年在生活上毫无变化,还是工作上班,回家游戏。 疫情还是带了我点什么的,至少让我在家里多呆了好久好久的时间。也算让我体验了在家 呆很久很久的感觉?讲道理,家里桌子椅子还有电脑都不舒服 TAT。都没法快乐的打游戏了。 回上海之后的隔离才是最开心的,还能在家里摸鱼。不过在家里的工作效率还是很高的, 毕竟在写东西的时候,是没人打扰的。仔细算下来,还比在办公室高一点呢!(而且摸鱼 的时间可确实是变多了哦) 疫情还让我变的更宅了?长久不说话,好像也让我更自闭了? 下半年的时候重新捡起了 WOW,现在回过头来看看,这个游戏本质上还是好玩的。至少比 现在的很多其他 MMORPG 要好玩很多。 12 月的时候,期待了很久的赛博朋克也发布了。但是我好像晕 3D… TAT! 0.3 0x03 要说工作上有啥不一样的,大概就是今年我们公司上市了? 不过反正我期权也不多啦,就也就赚一点? 其他工作上好像没啥特别有意思的内容,不过今年自己的成长还可以?自我感觉吧。 今年我一定一定要推荐的书是:A Philosophy of software design。这个对我现在 组织代码,修 bug 和新 feature 开发时候都很有帮助。 0.4 0x04 那先这样?第一次写这种东西… 还不太习惯…

2021/1/6
articleCard.readMore

搞了个 C++ 构建系统

我平时的工作内容是开发在服务端上运行的网络程序,主要语言是 C++,并且几乎全部跑在统一的机器环境上。所以我们一直以来都在使用一套简单从 blade-build 魔改来的编译系统。这个系统的本质是一个 Unix Makefile 的生成器,这个生成器使用 Python2 编写,代码有点难以维护。(毕竟使用动态语言的东西就是这样,出活还是很快的,但是时间一长了,代码就变的很难维护。因为后来的人,很难完全理解其中每个变量的类型,这对于我这种长期写静态强类型语言的人来说,是很痛苦的。) 在这样的基础上,我就萌生了把这套系统推倒重来的想法,这次我想用一个大家都熟悉,并且一定会写的语言来完成,它还一定是强类型的。在这几个条件下面,我超爱的 Rust 就被我排除在外了,因为好像我们周围会写的人也不是很多。再加上人家 cmake 也是用 C++ 来写的,我一想,我应该也比人家不差啥。再说,我还可以去 cmake 里面去偷很多实现出来。就这么决定了,用 C++ 来实现这个新的构建系统。 1 JK-Build 首先就是名字了,为啥叫 jk,那肯定有很多不可爱的人有奇奇怪怪的想法了!我当时起这个名字的时候,完全是图这两个按键在键盘上位于很近的位置,并且由于我是个 vim 的用户,其实手也常年在这两个按键附近,他们真的很近,很方便,就随手用了这个名字。 1.1 那现在的系统有啥问题呢 首先就是,原罪之一就是 Python。这个系统生成 Makefile 的速度在有的时候,它实在是有点慢了。 如果 BUILD 文件有改动了,它需要自己手动运行命令来重新生成。 对于 lint 来说,它的规则是尝试 lint 所有文件的,但是其实有很多文件是写出来放在那里,可以只搞了一半的(我就经常这样)。在这种时候我的代码是没法通过编译的,更不可能过 lint 了。 没有进度条。看看人家 cmake,还有个进度显示呢。这不就决定了我在编译的时候,是去喝杯咖啡,还是去拿个可乐嘛! 维护困难。Python 的代码有点难以维护,反正我在改的时候,是全靠着 print,到运行的时候观察每个变量到底是什么类型来 debug 的。 对于 clang-base 的工具,没有过匹配。大部分的,C++ 的代码检查和补全工具,其实都是建立在 clang 之上的,而 clang 会需要 compile database 来找到每个文件应该如何被编译,然后才能有 AST 或者其他的补全、提示什么的。 1.2 那和以前有什么不同呢 1.2.1 BUILD 文件和 targets 从 BUILD 文件来看,是没什么不同的: BUILD 文件依旧是 Python 的语法。 所有的目标构建依旧是一些 Python 的函数。 暂时来说,可用的构建目标类型也没有增加。(毕竟完全兼容旧系统就有很多的工作了) 新的系统,第一个目标就是完全兼容旧的所有已有的 BUILD 文件。毕竟,迁移的工作难度小了,新的系统才容易被大家接受。所以,所有的 BUILD 文件还依旧会是以前那样: python cc_library(name = "base", cppflags = [ "-Ilibrary", ], srcs = [ "*.cpp" ], excludes = [ "*_test.cpp" ], ldflags = [ "-ldl" ], deps = [ "//library/logging/BUILD:logging", "//library/memory/BUILD:memory", ":clock", ], ) 但是从可用的参数和意义上来说,还是有很多不同的。比如,构建目标 cc_library 现在有个参数叫 cppflags,现在我们就面临着这个参数的值没法继承的问题。往往如果在一个 target 里写了一个 -Ixxxx 的参数,在依赖它的 target 里面还是不可避免的要写上。所以在新的系统中,给 cc_library 以及它的所有派生目标都加入了可以继承的,includes 和 defines 参数,用来传递信息给依赖这个目标的目标。 对于这部分的变化,就去文档上看就好啦。(虽然现在还没有) 1.2.2 编译进度 还有进度条的加入。新的构建系统,在编译的时候,会有一个和 cmake 一样的计数进度的显示。这个地方 cmake 是通过统计一个目录下的文件个数来实现的,非常巧妙。具体的实现是,在一个编译命令的提示信息显示的同时,在一个特定的目录下也 touch 出来一个独一无二的文件,并且进度就应该是这个目录下的文件个数除以总的应有的文件个数。那如果文件没有改变怎么办?不会编译,那对应的计数不就没法统计进去了嘛?是的!所以,它还维护了一份总的计数给到一个 target 最后的命令上,这个命令是无论如何都会强制执行的,它会一次 touch 这个 target 应有的全部的文件。比如像这样: makefile all: lib.a .PHONY: all lib.a: a.o b.o c.o d.o e.o @jk-print --number=1,2,3,4,5,6 'lib.a' ... a.o: a.cpp a.h @jk-print --number=1 'compiling a.cpp' ... b.o: b.cpp b.h @jk-print --number=2 'compiling b.cpp' ... c.o: c.cpp c.h @jk-print --number=3 'compiling c.cpp' ... d.o: d.cpp d.h @jk-print --number=4 'compiling d.cpp' ... e.o: e.cpp e.h @jk-print --number=5 'compiling e.cpp' ... 这样就可以完美的把没有进行编译的文件的都补上了。 1.2.3 compile database 现在在生成 Unix Makefile 的同时,还会在项目的根目录生成一个 compile_commands.json 文件,作为其他 clang-base 的工具的基础。你可以把它和比如 clangd 或者 ccls 来配合。提供项目代码的补全、提示个高亮之类的功能。当然,如果你会自己写一些工具的话,就更好了! 1.2.4 内置的 cmake-base 的第三方库支持 原本使用一个第三方库,那可麻烦了。我们要自己写一个用来下载安装这个第三方库的一个脚本。自己要小心的处理好,下载文件的逻辑,是不是使用的代理。文件下载之后是不是完整,要不要进行 md5 校验。文件怎么解压,包括后面的 cmake 命令怎么搞,怎么填写这一系列事情。虽然我尝试将一部分工作,封装在了一些内置的包里面,但是依旧有点麻烦。所以这次趁着重写,干脆就重新完整的提供了一个内置的支持! python cmake_library( url = "http://xxxxx.xxx/xxx.tar.gz", sha256 = "xxxxxx", type = "tar.gz", header_only = False, job = 10, ) 1.2.5 独立的进程 现在终于不再依赖,所有的项目都是 media_build 的一个子目录了!你可以任意组织你的项目目录,可以随意的把不管是 libray 还是 protocol 都作为一个 sub-project 放在自己的项目目录里,用自己想用的任意方式来管理! 1.3 最后的最后 不过,现在还在开发过程啦。 还有一些对以前的兼容没有实现。但是这也已经是一个全新的构建系统了!最初搞的时候,不光考虑了兼容,很多新的功能还有参数设计,也参考了 blaze,buck 还有 cmake 的样子。也应该算是取出来这三个里面,我认为用起来比较方便,舒服的部分吧。 不过缺点还是,这个系统的最初目标就是构建服务端的程序的,对于环境方面没有过多的考虑。所以基本不兼容 windows,macos 能不能用还有全看缘分。这个就没啥计划了… 要是恰好能用!那可太好了!

2020/9/14
articleCard.readMore

软件设计哲学(NOTE)

1 RED FLAGS 1.1 Shallow Module A shallow module is one whose interface is complicated relative to the functionality it provides. Shallow modules don’t help much in the battle against complexity, because the benefit they provide (not having to learn about how they work internally) is negated by the cost of learning and using their interfaces. Small modules tend to be shallow. 1.2 Information Leakage Information leakage occurs when the same knownledge is used in multiple places, such as two different classed that both understand the format of a particular type of file. 1.3 Temporal Decomposition In temporal decomposition, execution order is reflected in the code structure: operations that happen at different times are in different methods or classes. If the same knowledge is used at different points in execution, it get encoded in multiple places, resulting in information leakage. 1.4 Overexposure If the API for a commonly used feature forces users to learn about other features that are rarely used, this increases the cognitive load on users who don’t need the rarely used features. 1.5 Pass-Through Method A pass-through method is one that does nothing except pass its arguments to another method, usually with the same API as the pass-through method. This typically indicates that there is not a clean division of respoinsibility between the classes. 1.6 Repetition If the same piece of code (or code that is almost the same) appears over and over again, that’s a red flag that you haven’t found the right abstractions. 1.7 Special-General Mixture This red flag occurs when a general-purpose mechanism also contains code specialized for a particular use of that mechanism. This makes the mechanism more complicated and creates information leakage between the 80 mechanism and the particular use case: future modifications to the use case are likely to require changes to the underlying mechanism as well. 1.8 Conjoined Methods It should be possible to understand each method independently. If you can’t understand the implementation of one method without also understanding the implementation of another, that’s a red flag. 1.9 Comment Repeats Code Red Flag: Comment Repeats Code If the information in a comment is already obvious from the code next to the comment, then the comment isn’t helpful. One example of this is when the comment uses the same words that make up the name of the thing it is describing. 1.10 Implementation Documentation Contaminates Interface This red flag occurs when interface documentation, such as that for a method, describes implementation details that aren’t needed in order to use the thing being documented. 1.11 Vague Name If a variable or method name is broad enough to refer to many different things, then it doesn’t convey much information to the developer and the underlying entity is more likely to be misused. 1.12 Hard to Pick Name If it’s hard to find a simple name for a variable or method that creates a clear image of the underlying object, that’s a hint that the underlying object may not have a clean design. 1.13 Hard to Describe The comment that describes a method or variable should be simple and yet complete. If you find it difficult to write such a comment, that’s an indicator that there may be a problem with the design of the thing you are describing. 2 EXCERPT 2.0.1 Definition Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system. $$ C = \sum_{p}c_pt_p $$ The overall complexity of a system $(C)$ is determined by the complexity of each part $p(c_p)$ weighted by the faction of time developers spend working on the part $(t_p)$. 2.0.2 Symptoms Change amplification: The first symptom of complexity is that a seemingly simple change requires code modifications in many different places. Cognitive load: The second symptom of complexity is cognitive load, which refers to how much a developer needs to know in order to complete a task. Unknown unknowns: The third symptom of complexity is that it is not obvious which pieces of code must be modified to complete a task, or what information a developer must have to carry out the task successfully. 2.1 Modules In this world, the complexity of a system would be the complexity of its worst module. The interface describes what the module does but not how it does it. The best modules are those whose interfaces are much simpler than their implementations. An abstraction is a simplified view of an entity, which omits unimportant details. Interface: The interface to a module contains two kinds of information: formal and informal. The formal parts of an interface are specified explicitly in the code, and some of these can be checked for correctness by the programming language. The informal parts of an interface includes its high-level behavior, such as the fact that a function deletes the file named by one of its arguments. In modular programming, each module provides an abstraction in form of its interface. The interface presents a simplified view of the module’s functionality; the details of the implementation are unimportant from the standpoint of the module’s abstraction, so they are omitted from the interface. A detail can only be omiited from an abstraction if it is unimportant. The best modules are those that provide powerful functionality yet have simple interfaces: they have a lot of functionality hidden behind a simple interface. A deep module is a good abstraction because only a small fraction of its internal complexity is visible to its users. Classitis may result in classes that are individually simple, but it increases the complexity of the overall system. Small classes don’t contribute much functionality, so there have to be a lot of them, each with its own interface. These interfaces accumulate to create tremendous complexity at the system level. Provide choice is good, but interfaces should be designed to make the common case as simple as possible. If an interface has many features, but most developers only need to be aware of a few of them, the effective complexity of that interface is just the complexity of the commonly used features. The most important (and perhaps surprising) benefit of the general- purpose approach is that it results in simpler and deeper interfaces than a special-purpose approach. The general-purpose approach can also save you time in the future, if you reuse the class for other purposes. However, even if the module is only used for its original purpose, the general-purpose approach is still better because of its simplicity. Questions to ask yourself What is the simplest interface that will cover all my current needs? In how many situations will this method be used? Is this API easy to use for my current needs? Decorators A decorator object takes an existing object and extends its functionality; it provides an API similar or identical to the underlying object, and its methods invoke the methods of the underlying object. Before creating a decorator class, consider alternatives such as the following: Could you add the new functionality directly to the underlying class, rather than creating a decorator class? If the new functionality is specialized for a particular use case, would it make sense to merge it with the use case, rather than creating a separate class? Could you merge the new functionality with an existing decorator, rather than creating a new decorator? Finally, ask yourself whether the new functionality really needs to wrap the existing functionality: could you implement it as a stand-alone class that is independent of the base class? 2.1.1 Together vs Apart Bring together if information is shared Bring together if it will simplify the interface Bring together to eliminate duplication 2.1.2 Comments Documentation also plays an important role in abstraction; without comments, you can’t hide complexity. If users must read the code of a method in order to use it, then there is no abstraction: all of the complexity of the method is exposed. Many of the most important comments are those related to abstractions, such as the top-level documentation for classes and methods. The overall idea behind comments is to capture information that was in the mind of the designer but couldn’t be represented in the code. Comments should describe things that aren’t obvious from the code. One of the most important reasons for comments is abstractions, which include a lot of information that isn’t obvious from the code. Developers should be able to understand the abstraction provided by a module without reading any code other than its externally visible declarations. Comments categories: Interface: a comment block that immediately precedes the declaration of a module such as a class, data structure, function, or method. The comment describe’s the module’s interface. Data structure member: a comment next to the declaration of a field in a data structure, such as an instance variable or static variable for a class. Implementation comment: a comment inside the code of a method or function, which describes how the code works internally. Cross-module comment: a comment describing dependencies that cross module boundaries. After you have written a comment, ask yourself the following question: could someone who has never seen the code write the comment just by looking at the code next to the comment? If the answer is yes, as in the examples above, then the comment doesn’t make the code any easier to understand. Comments like these are why some people think that comments are worthless. Comments augment the code by providing information at a different level of detail. Precision is most useful when commenting variable declarations such as class instance variables, method arguments, and return values. The name and type in a variable declaration are typically not very precise. Comments can fill in missing details such as: What are the units for this variable? Are the boundary conditions inclusive or exclusive? If a null value is permitted, what does it imply? If a variable refers to a resource that must eventually be freed or closed, who is responsible for freeing or closing it? Are there certain properties that are always true for the variable (invariants), such as “this list always contains at least one entry”? When documenting a variable, think nouns, not verbs. In other words, focus on what the variable represents, not how it is manipulated. The best time to write comments is at the beginning of the process, as you write the code. Writing the comments first makes documentation part of the design process. Not only does this produce better documentation, but it also produces better designs and it makes the process of writing documentation more enjoyable. 2.1.2.1 Interface Comments The interface comment for a method includes both higher-level information for abstraction and lower-level details for precision: The comment usually starts with a sentence or two describing the behavior of the method as perceived by callers; this is the higher-level abstraction. The comment must describe each argument and the return value (if any). These comments must be very precise, and must describe any constraints on argument values as well as dependencies between arguments. If the method has any side effects, these must be documented in the interface comment. A side effect is any consequence of the method that affects the future behavior of the system but is not part of the result. For example, if the method adds a value to an internal data structure, which can be retrieved by future method calls, this is a side effect; writing to the file system is also a side effect. A method’s interface comment must describe any exceptions that can emanate from the method. If there are any preconditions that must be satisfied before a method is invoked, these must be described (perhaps some other method must be invoked first; for a binary search method, the list being searched must be sorted). It is a good idea to minimize preconditions, but any that remain must be documented. 2.1.2.2 Implementation Comments The main goal of implementation comments is to help readers understand what the code is doing (not how it does it).

2020/6/1
articleCard.readMore

Paxos Note

0.1 Symbols And Structure 表决 $B$ rs struct Ballot { dec: Decree, // 表决的内容 vot: Set<Node>, // 表决投票通过的节点 qrm: Set<Node>, // 表决参与的节点 bal: u64, // 表决编号 } 投票 $v$ rs struct Vote { pst: Node, // 本投票的节点 bal: u64, // 本投票的表决编号 dec: Decree, // 本投票表决的内容 } 表决的集合 $\beta$ 0.2 Define Some Useful functions $Votes(\beta)$:所有在 $\beta$ 中的表决的投票的集合 $$Votes(\beta) = \{v:(v_{pst}\in B_{vot})\cap(v_{bal}=B_{bal}), B \in \beta\}$$ $Max(b, p, \beta)$:在由节点 $p$ 投给 $\beta$ 中的表决的投票中,编号小与等于 $b$ 的最大投票 $$Max(b, p,\beta)=max\{v \in Votes(\beta):(v_{pst}=p)\land(v_{bal}<b)\}\cup\{null_{p}\}$$ $MaxVote(b, Q, \beta)$:在集合 $Q$ 中的任意一个节点投给 $\beta$ 中的表决的投票中,编号小于等于 $b$ 的最大投票 $$MaxVote(b,Q,\beta)=max\{v\in Votes(\beta):(v_{pst}\in Q)\cap(v_{val}<b)\}\cup\{null_p\}$$ 那么如果条件$B1(\beta)-B3(\beta)$满足的情况下,那么系统将满足一致性,并且是可进展的。 $B1(\beta) \triangleq \forall B,B’ \in \beta:(B \ne B’) \implies (B_{bal} \ne B’_{bal})$ $B2(\beta) \triangleq \forall B,B’ \in \beta:B_{qrm}\cap B’_{qrm} \ne \emptyset $ $B3(\beta) \triangleq \forall B \in \beta: (MaxVote(B_{bal},B_{qrm},\beta)_{bal}\ne - \infty) \implies B_{dec} = MaxVote(B_{bal}, B_{qrm}, \beta)_{dec} $ 0.2.1 Lemma 1 如果 $\beta$ 中的表决 $B$ 是成功的,那么 $\beta$ 中更大编号的表决和 $B$ 的表决内容相同。 $$ ((B_{qrm} \subseteq B_{vot})\land(B’_{bal}>B_{bal})) \implies (B’_{dec}=B_{dec}) $$ 0.2.2 Proof 定义集合 $\Psi(B, \beta)$: $\Psi(B, \beta) \triangleq \{B’\in \beta:(B’_{bal}>B_{bal})\land(B’_{dec}\ne B_{dec}) \}$,表示 $\beta$ 中编号比 $B$ 大并且表决内容不相同的表决的集合。 $ C = min\{B’:B’\in \Psi(B, \beta)\} $ $ C_{bal} < B_{bal} $ $ C_{qrm} \cap B_{bot} \ne \emptyset $ 因为 $B2$ 和 假设中的 $B$ 表决是成功的,也就是 $ B_{qrm} \subseteq B_{vot} $ $ MaxVote(C_{bal},C_{qrm},\beta)_{bal} \ge B_{bal} $ 因为 $C_{qrm}$ 和 $B$ 的投票者一定有交集 $ MaxVote(C_{bal}, C_{qrm}, \beta)\in Votes(\beta)$ $ MaxVote(C_{bal}, C_{qrm}, \beta)_{dec} = C_{dec} $ $ MaxVote(C_{bal}, C_{qrm}, \beta)_{dec} \ne B_{dec} $ $ MaxVote(C_{bal}, C_{qrm}, \beta)_{bal} > B_{bal} $ $ MaxVote(C_{bal}, C_{qrm}, \beta) \in Votes(\Psi(B, \beta)) $ $ MaxVote(C_{bal}, C_{qrm}, \beta)_{bal} < C_{bal} $ 9, 10 和 1 矛盾。 0.2.3 定理 1 在满足 $B1(\beta)$,$B2(\beta)$,$B3(\beta)$ 的情况下, $$((B_{qrm} \subseteq B_{vot})\land(B’_{qrm}\subseteq B’_{vot})) \implies (B’_{dec} = B_{dec}) $$ 0.2.4 定理 2 $$ \forall B\in\beta, b > B_{bal}, Q \cap B_{qrm} \ne \emptyset $$ 如果 $B1(\beta)$,$B2(\beta)$,$B3(\beta)$ 满足,那么存在一个 $ B’, B’_{bal}=b, B’_{qrm}=B’_{vot}=Q $ 使得 $B1(\beta\cup\{B’\})$,$B2(\beta\cup\{B’\})$,$B3(\beta\cup\{B’\})$ 成立。

2020/5/24
articleCard.readMore

关于 cpp 可见性的黑魔法后门

这个点子是我最初在网上其他地方看到的,感觉很好玩。就记录一下啦。 Reference: http://cpp.kjx.cz/private_backdoor.html

2020/5/14
articleCard.readMore

一个关于 private member function detect 的 SFINAE 模板

1 问题的开始 问题起源于,我要搞一个模板,来检查一个类,是不是有一个特定的回调接口 OnAlarm()。我显然希望在我的模板类里面,直接调用这个 OnAlarm 回调的。但是问题,就这么出现了。我需要一个模板,来检查一个传给构造函数的指针指向的类型,是不是有我需要的 OnAlarm 方法。如果没有的话,我需要使用另一套回调的机制。 问题就出在了这个检查上面。 2 问题的最初的样子 最开始的时候,我是写成了这个样子的。 cpp template <typename T, typename = void> struct HasAlarmCallback : std::false_type {}; template <typename T> struct HasAlarmCallback<T, decltype(std::declval<T>().OnAlarm())> : std::true_type {}; 这样看起来是没问题的(其实也是没问题的),但是我遇到了第一个问题。 c++ class B { void OnAlarm() {} }; 这个模板在 T = B 的时候,会有编译错误,并不能成功的使用 SFINAE。错误的内容大概就是,这里用了一个 private 的函数,然后是不可见的。 3 曙光 (虽然也不知道为什么) c++ template <typename T, typename = void> struct HasAlarmCallback : std::false_type {}; template <typename T> struct HasAlarmCallback<T, decltype(static_cast<T*>(nullptr)->OnAlarm())> : std::true_type {}; struct A { void OnAlarm(){}; }; class B { void OnAlarm(){}; }; int main() { HasAlarmCallback<A> a; HasAlarmCallback<B> b; } 在我这样写的时候,代码是可以完美通过编译,并且可以完美运行的。结果也是完美符合预期的。那我就会想呀,为啥我直接像下面样子写就不对了呢~ c++ std::cout << HasAlarmCallback<A>::value << std::endl; std::cout << HasAlarmCallback<B>::value << std::endl; !!!这是我百思不得其解的要点!!! 在我这么写的时候,就不 work,会报像最开始那样的编译问题。 4 最后的方案 c++ template<typename T, typename = void> struct _HasAlarmCallback { static constexpr bool value = false; }; template<typename T> struct _HasAlarmCallback<T, decltype(static_cast<T *>(nullptr)->OnAlarm())> { static constexpr bool value = true; }; template<typename T> struct HasAlarmCallback { private: static constexpr _HasAlarmCallback<T> foo{}; public: static constexpr bool value = _HasAlarmCallback<T>::value; }; 我最后发现,如果需要我构造一个实例才能解决这个问题的话,那我就构造一个。 嗯… 虽然… 不知道为啥… 反正是 work 了… 如果有人看到,然后知道为什么的话… 请告诉我!谢谢了!

2019/12/5
articleCard.readMore

User-defined conversion and Copy elision

5 问题的开始 问题的开始是同事聊到了我们笔试题的一个问题,是说下面这个代码其实在编译的时候是有问题的。 cpp struct UserInfo { UserInfo(const std::string& name) : name_(name) {} private: std::string name_; }; int main() { UserInfo u = "name"; } 6 最初的讨论和思考 显然在最开始的时候,我并没有发现这个代码的问题所在,并且被告知了在这段代码里面其实是有两个问题的。 在这个简单的例子里面,就涉及到了在规范里面两个很不容易注意到的行为,就像标题里聊的 UDC(user-defined conversion) 和 copy-elision。 6.1 关于隐式转换(implicit conversion) 在 main 函数里面唯一的语句,这首先是一个变量的声明和定义,同时还包括了这个变量的初始化(initialize)。在这个变量的初始化阶段,发生了几次类型转换,其中大部分都是隐式的(implicit conversion),并且调用到了不同的构造函数: text const char[5] =[implicit conversion(1)]=> std::string =[implicit conversion(2)]=> UserInfo =[copy/move(3)]=> UserInfo 第一次发生在字符串字面量(string literal)构造 std::string 的时候,显然这是一个隐式转换,因为并没有显式的调用 std::string 的构造函数,并且这个隐式转换显然是 user-defined 的。 第二次发生在 std::string 构造一个 UserInfo 的时候,这也是一个隐式转换,并且是 user-defined 的。 这两次隐式转换构成了一个隐式转换链(implicit-conversion sequence),问题就出在这个由两个 user-defined conversion 构成的隐式转换链上。在标准的 16.3.3.1 里讨论了有关于 implicit conversion 和 user-defined conversion 的部分,而在 15.3 里特别提到了: At most one user-defined conversion (constructor or conversion functions) is implicitly applied to a single value. 在一个隐式转换序列里,只能存在最多一个用户定义的转换。这个条件在标准的隐藏的很深,我在通读标准的时候几次都错过了他们。(但是据说这个问题,曾经在邮件列表里有过蛮激烈的讨论的,但是可惜我那个时候还是个孩子hhh) 所以在这个问题里,发生 ill-formed 的第一个原因是,在一个 implicit conversion sequence 里面,存在多个 user-defined conversion。 6.2 关于复制消除(copy elision) 关于复制消除的部分,这里就要提到不同的两个版本,C++17 开始和 C++17 之前。 在 C++17 之前,并没有明确的提出在什么情况下,可以彻底进行复制消除(这里的彻底的指的是包括不进行是否有可用的 copy/move 构造函数的检查)。 所以在 C++17 之前,下面的这段代码是会有编译错误的: cpp struct Foo { Foo(int) {} Foo(Foo&&) = delete; Foo(const Foo&) = delete; }; int main() { Foo a = 1; } 可以考虑上面给出的上面那个问题的隐式转换链,这个首先出现的是 source type 的 target type 的不一致,所以出现了一次 user-defined 的 implicit conversion。从类型 int 得到了类型 Foo 的一个 prvalue(这里的 prvalue 很重要)。然后才是从一个 prvalue 构造一个类型 Foo 的对象。 其实我们显然可以知道,第二个过程是会被优化掉的,一般的编译器都会优化成原地构造的。但是标准在这个时候要求了,在这种时候,即使这部分的内容会优化,但是依旧要进行编译时的检查,检查 copy/move 构造函数是否可用,如果不可用,那这个代码依旧是 ill-formed。 但是事情在 C++17 中发成了一个很大的改变 11.6.4.3.9,在一个是 prvalue 的时候,这里会用 direct-initalize,而不是尝试使用 copy/move initialize。也就是说,上面例子的代码在 C++17 之后其实是可以通过编译的。 但是这里要注意,适用的规则是一个 prvalue 的对象,xrvalue 是不可以的。也就是说,下面这样的代码依旧是不能通过编译的: cpp struct Foo { Foo(int = 10) {} Foo(Foo&&) = delete; Foo(const Foo&) = delete; }; struct Bar { operator Foo&& { return std::move(a); } Foo a; }; int main() { Bar b; Foo a = b; } 这里虽然做了一次隐式类型转换(从 Bar 到 Foo),但是得到的类型是一个 xrvalue,而 xrvalue 是不适合上面的拷贝消除规则的,所以还会尝试使用 copy/move 构造,得到 ill-formed 的结果。 文中提到的所有标准文档,都采用最新的 C++ 标准文档(ISO/IEC 14882 2017),除非特别指定此时讨论的C++版本。

2019/4/19
articleCard.readMore

VIM and Latex

1 起源 起源是在群里看到了有人分享的关于一个人用 vim 写 latex 的文章,但是它的做法是用了一个 vimtex 的独立插件。 我是个 language server 的狂热使用者,所以我就在找一个用 language server 的处理方案。回忆起来另一次在另一个群里看到的,一个叫做 texlab 的项目,就在 vim 里搞个配合。 2 vim 里的插件选择 vim 里的 language client 的实现用好几种: vim-lsp coc.nvim LanguageClient-neovim 用来用去还是 coc.nvim 在这里面不论是流畅度还是 feature 的丰富程度都是比较好的。所以我也一直在用。 3 Install install coc.nvim: vim call dein#add('neoclide/coc.nvim', {'build': 'yarn install'}) install texlab: bash git clone https://github.com/latex-lsp/texlab yarn install && yarn build 4 config config in coc.nvim json "texlab": { "command": "node", "args": [ "/path/to/texlab/dist/texlab.js", "--stdio" ], "filetypes": ["tex", "plaintex"], "trace.server": "verbose" }

2019/3/27
articleCard.readMore

Compare Between CRTP and Virtual

我们平时都会使用虚函数来实现 C++ 里的运行时的多态,但是虚函数会带来很多性能上面的问题: 虚函数的调用需要额外的寻址 虚函数不能被 inline,当使用比较小的虚函数的时候会带来很严重的性能负担 需要在每个对象中维护一个额外的虚函数表 但是在有些情况下,我们就可以用一些静态的类型分发策略来带来一些性能上面的好处。 1 一个传统的例子 c++ struct VirtualInterface { virtual void Skip(uint32_t steps) = 0; }; struct VirtualImpl : public VirtualInterface { uint32_t index_; void Skip(uint32_t steps) override { index_ += steps; index_ %= INT_MAX; } }; void VirtualRun(VirtualInterface* interface) { for (auto i = 0; i < N; i++) { for (auto j = 0; j < i; j++) { interface->Skip(j); } } } 这里有一个很简单的例子,我们搞了一个简单的计数类来模拟这个过程。首先使用虚函数的方法去实现这个。在开了O2的情况下,运行了 3260628226 ns。 然后我们使用 CRTP 来实现: c++ template <typename Impl> struct CrtpInterface { void Skip(uint32_t steps) { static_cast<Impl*>(this)->Skip(steps); } }; struct CrtpImpl : public CrtpInterface<CrtpImpl> { void Skip(uint32_t steps) { index_ += steps; index_ %= INT_MAX; } uint32_t index_ = 0; }; template <typename T> void CrtpRun(CrtpInterface<T>* interface) { for (auto i = 0; i < N; i++) { for (auto j = 0; j < i; j++) { interface->Skip(j); } } } 同样运行我们的代码, 29934437 ns。 显然在省去了查虚函数表,并且可以inline的情况下,程序有了更好的表现。 在具体的实现方式上,参考上面的实现就可以了…

2018/10/16
articleCard.readMore

Interface in C++

1 Interface In C++ 1.1 问题提出 我记得我不止一次提到说,我更喜欢 golang 的泛型设计。一个优秀的泛型系统,我希望是来表示一个方法可以接受什么。应该是一个类似于 concept 的概念。我们都知道,在 C++ 里面,我们更多的使用虚函数来实现这个功能,就像下面这样: c++ struct IPerson { virtual std::string Name() = 0; virtual uint32_t Age() = 0; }; 我们搞了一个几个纯虚函数来表示一个接口类,然后我们都会搞一些类来继承这个类,就像下面这样: c++ struct Student : public IPerson { std::string Name() override; uint32_t Age() override; }; 那在使用的地方: c++ void Foo(IPerson*); 这已经是我们一般在写代码时候的常规做法了,但是在这样的情况下,我们要求了所有使用这个的地方,都只能使用指针或者引用。因为我们不能对一个对象来搞这些东西。 再回想一下,golang 的泛型的样子,我们有没有一个办法,可以搞一个类似于 interface 的东西来让一个对象也可以表示这些东西呢? 1.2 简单思考1 基于上面的问题,考虑这个问题的背后是表示的是个什么类型的多态问题。Ok,显然是个运行时多态。编译时多态的问题可以配合 template, constexpr 来解决。那么运行时多态在原本的 C++ 是通过虚函数来解决的。虚函数的实现,又是通过一个虚函数表来实现的。那么问题来了,我们可不可以自己来维护一个虚函数表来达到我们想要的效果呢? 上面我们需要的接口类,显然我们可以提炼出来一个这样的虚函数表: c++ struct vtable { std::string (*Name)(void*); uint32_t (*Age)(void*); }; 这个虚函数表表示了这个接口需要哪些接口,在这里使用 void* 来表示任意类型的指针。 那有了这个虚函数表之后,我们应该怎么使用这个呢?就像这个这样: c++ template<typename T> vtable vtable_for = { [](void* p) { return static_cast<T*>(p)->Name(); }, [](void*) { return static_cast<T*>(p)->Age(); }, }; 这里用了 C++14 的新特性:变量模板,来构造了一个静态的全局变量,来表示对应的制定类型的虚函数表实现。 在有了上面两个东西的基础上,就得到了接口类的实现: c++ struct Person { template<typename T> Person(T t) : vtable_( &vtable_for<T> ), p(new T{t}) {} private: vtable* vtable_; void* p; }; 接下来,这个类就可以很棒了。你可以像下面这么定义: c++ std::vector<Person> persons; persons.push_back(Student{}); persons.push_back(Teacher{}); ... 用起来的时候,一切都看起来和 golang 的那个版本差不多了呢。 Reference: https://zh.wikipedia.org/wiki/C%2B%2B14#%E5%8F%98%E9%87%8F%E6%A8%A1%E6%9D%BF

2018/8/6
articleCard.readMore

Compile Time Reflection in C++11

1 故事背景 故事发生在遥远的我在使用C++来处理JSON和对象绑定的时候,我厌倦了写这样的代码: c++ class Foo { int bar1; int bar2; int bar3; std::string bar4; int bar5; std::string ToJsonString(); }; std::string Foo::ToJsonString() { Document doc; doc.SetObject(); doc.AddMember("bar1", Value(bar1), doc.GetAllocator()); doc.AddMember("bar2", Value(bar2), doc.GetAllocator()); doc.AddMember("bar3", Value(bar3), doc.GetAllocator()); doc.AddMember("bar4", Value(bar4), doc.GetAllocator()); doc.AddMember("bar5", Value(bar5), doc.GetAllocator()); ... } 这样的代码又复杂又容易出错,所以我就在考虑一种可以自动的将这些东西都完成好的绑定方法。所以就有了文章写的内容。 2 预备部分 2.1 模板类 我们需要一个可以在编译时期被构造的字符串类,用于保存我们需要反射的类的类名和成员名。在C++17之后,我们可以使用标准库中提供的std::string_view来实现,但是在C++11中,我们没有这样的实现,就只能用constexpr的构造函数来实现一个我们自己的std::string_view类。 这部分在C++11中的实现可以参考我在另一篇文章中的实现。编译时期常量数组及常用操作 这里给出一个简单的实现: c++ class ConstString { public: template<uint32_t N> constexpr ConstString(const char (&arr)[N]) : begin_(arr), size_(N-1) { static_assert(N >= 1, "const string literal should not be empty"); } constexpr ConstString(const char* buf, uint32_t len) : begin_(buf), size_(len) {} constexpr char operator[](uint32_t i) const { return begin_[RequiresInRange(i, size_)]; } constexpr const char* Begin() const { return begin_; } constexpr const char* End() const { return begin_ + size_; } constexpr uint32_t Size() const { return size_; } constexpr ConstString SubString(int pos, int len) const { return RequiresInRange(pos, size_), RequiresInRange(pos + len, size_), ConstString(begin_ + pos, len); } private: const char* const begin_; uint32_t size_; }; constexpr bool operator==(ConstString a, ConstString b) { return a.Size() != b.Size() ? false : StringEqual(a.Begin(), b.Begin(), a.Size()); } 这个实现提供了在字符串上的几个基本操作,后面的实现中可以根据自己的需要扩展。 2.2 宏(macro) 2.2.1 宏参数个数 我们都知道,在C++11中提供了变长参数模板,可以让我们接受任意个任意类型的参数: c++ template<typename... Args> void fuck(Args... args); 还有技巧可以帮助我们写出类型处理正确的、零开销的完美转发;有扩展的用法sizeof...(Args)来帮助我们获得参数包中参数的个数。 但是,如果我们想得到一个宏的变长参数包中参数的个数呢?有什么宏展开的技巧可以帮助我们做到这一点呢。答案显然是有的,我们用两个宏来配合我们做到这一点: c++ #define __RSEQ_N() 5, 4, 3, 2, 1, 0 #define __ARG_N(_1, _2, _3, _4, _5, N, ...) N 上面的宏考虑其展开的过程: c++ __ARG_N(a, b, c, __RSEQ_N()) // 1:调用 __ARG_N(a, b, c, 5, 4, 3, 2, 1, 0) // 2:展开1 考虑展开后的形式: c++ __ARG_N( a, b, c, 5, 4, 3, 2, 1, 0) // __ARG_N(_1, _2, _3, _4, _5, N, ...) N 我们可以明显的得到__ARG_N这个宏在这种情况下展开的结果为3。我们再对这个宏进行简单的包装,就得到了一个易用的获得宏参数个数的宏。 c++ #define __GET_ARG_COUNT_INNER(...) __ARG_N(__VA_ARGS__) #define __GET_ARG_COUNT(...) __GET_ARG_COUNT_INNER(__VA_ARGS__, __RSEQ_N()) 对这个宏进行一些简单的测试: c++ assert(__GET_ARG_COUNT(a,), 1); assert(__GET_ARG_COUNT(a, b), 2); assert(__GET_ARG_COUNT(a, b, c), 3); assert(__GET_ARG_COUNT(a, b, c, d), 4); assert(__GET_ARG_COUNT(a, b, c, d, e), 5); 通过扩展宏__RSEQ_N()和宏__ARG_N来扩展其所支持的参数个数。简单的增加宏里的参数个数和数值即可。 2.2.2 构造字符串序列 我们都知道,在宏里面可以通过使用#来将一个宏参数用引号来括起来,形成字符串的形式。那么利用这个特性,我们就可以得到一个参数的字符串形式和我们上面完成的常量字符串对象。 c++ #define __ADD_VIEW(str) ConstString(#str) 并且通过宏的递归来实现生成一个常量字符串对象的序列: c++ #define __CONST_STR_1(str, ...) __ADD_VIEW(str) #define __CONST_STR_2(str, ...) __ADD_VIEW(str), __CONST_STR_1(__VA_ARGS__) #define __CONST_STR_3(str, ...) __ADD_VIEW(str), __CONST_STR_2(__VA_ARGS__) ... 以此类推可以得到你想要的个数的形式。【如果你在使用VIM的话,这里的代码可以简单的使用VIM的宏功能来完成。(使用q来录制一个宏,C-A来自增当前位置的数字)。VIM最棒啦。我就是这么完成的UoU】 上面的宏,将被展开成这样: c++ // __CONST_STR_3(a, b, c) ConstString("a"), ConstString("b"), ConstString("c") 2.3 将参数序列转成字符串序列 先搞一个简单的宏把两个名字连起来成为一个名字: c++ #define __MACRO_CONCAT(m1, m2) __MACRO_CONCAT_IMPL(m1, m2) #define __MACRO_CONCAT_IMPL(m1, m2) m1##_##m2 然后结合我们上面完成的两个宏,就可以啦: c++ #define __MAKE_STR_LIST(...) __MACRO_CONCAT(__CONST_STR, __GET_ARG_COUNT(__VA_ARGS__))(__VA_ARGS) 将__CONST_STR这个名字和参数个数连起来,就是其在我们上面实现的第二个宏的名字,比如:__CONST_STR_1、__CONST_STR_2等等。然后再调用这个宏即可。 2.4 将一个操作写入所有的宏参数 在这里使用类似字符串转换那里的技巧,可以很容易的得到一个宏: c++ #define __MAKE_ARG_LIST_1(op, arg, ...) op(arg) #define __MAKE_ARG_LIST_2(op, arg, ...) op(arg), __MAKE_ARG_LIST_1(op, __VA_ARGS__) ... 上面的宏被使用时,将这样被展开: c++ #define __FIELD(t) t // __MAKE_ARG_LIST_3(&Name::__FIELD, a, b, c) &Name::a, &Name::b, &Name::c 3 使用一个类来保存这些宏信息 在这里我希望构造一个类似这样的结构体来保存一个类的成员的宏信息: c++ struct Name { char* rname; }; struct __reflect_struct_Name { using size_type = std::integral_constant<size_t, 1>; constexpr static ConstString Name() { return ConstString("Name"); } constexpr static size_t Value() { return size_type::value; } constexpr static std::array<ConstString, size_type::value> MembersName() { return std::array<ConstString, 1>{{ ConstString("rname") }}; } constexpr decltype(std::make_tuple(&Name::rname)) static MembersPointer() { return std::make_tuple(&Name::rname); } }; 观察我们上面的几个宏,可以显然得到这样的一种写法: c++ #define __MAKE_REFLECT_CLASS(StructName, ...) \ struct __reflect_struct_##StructName { \ using size_type = std::integral_constant<size_t, __GET_ARG_COUNT(__VA_ARGS__)>; \ constexpr static ConstString Name() { \ return ConstString(#StructName); \ } \ constexpr static size_t Value() { \ return size_type::value; \ } \ constexpr static std::array<ConstString, size_type::value> MembersName() { \ return std::array<ConstString, size_type::value>{{ \ __MACRO_CONCAT(__CONST_STR, __GET_ARG_COUNT(__VA_ARGS__))(__VA_ARGS__) \ }}; \ } \ constexpr static decltype(std::make_tuple()) static MembersPointer() { return std::make_tuple( \ __MACRO_CONCAT(__MAKE_ARG_LIST, &StructName::__FIELD, __VA_ARGS__) \ ); \ } \ }; 上面的这个宏可以帮助我们构造一个结构体,在结构体里的分别用Name()方法来返回其保存的元信息类型名,用MembersName()返回保存类型的所有成员名,用MembersPointer()返回保存类型的所有成员指针。 然后利用函数的重载来返回这个结构体: c++ __reflect_struct_##StructName __reflect_structs(StructName const&) { \ return __reflect_struct##StructName{}; \ } 4 使用模板函数来获取这些元信息 c++ template<typename T> constexpr const ConstString GetName() { return decltype(__reflect_structs(std::declval<T>()))::Name(); } template<typename T> constexpr const ConstString GetName(size_t i) { return decltype(__reflect_structs(std::declval<T>)))::MembersName()[i]; } 后面的思想基本就都和这个类似,利用模板和函数重载来获取这些类型的元信息。 5 结合其他宏使用 在使用这种操作的时候,我们需要使用一个宏来构造我们上面提到的所有元信息,这个应该是一个没有办法的事情了。为了这个功能这些多出来的代码,我也是可以接受的。 当然如果这个类型本来就是使用宏构造出来的话,就可以把这两个宏很舒服的结合在一起啦~所以我也推荐你这么用哦。

2018/4/15
articleCard.readMore

C++11内存模型

1 Introduction // cpp concurrency in action 里的例子 c++ void undefined_behaviour_with_double_checked_locking() { if (!resource_ptr) { // 1 std::lock_guard<std::mutex> lk(resource_mutex); if (!resource_ptr) { // 2 resource_ptr.reset(new some_resource); // 3 } } resource_ptr->do_something(); // 4 } 在 C++ Concurrency in Action 中提到过一段很有意思的代码,这段代码存在潜在的条件竞争。未被锁保护的操作①,没有与另一个线程中被锁保护了的操作③进行同步,在这样的情况下就有可能产生条件竞争。这个操作不光会覆盖指针本身,还有会影响到其指向的对象。所以在后面的操作④就有可能会导致不正确的结果。 2 Out-of-order process 让我们从一个稍微简单一点的例子聊起。考虑现在我们的程序有两个线程,他们持有共同的变量 a 和 b,其中 a 和 b 的初始值都是0,两个线程分别执行了这样的代码: 线程1 c++ b = 1; // 1 a = 1; // 2 线程2: c++ int a = 0, b = 0; while (a == 0); assert(b == 1); // 3 在这个例子里面,我们可以保证每次在位置①的断言都可以成功么? 显然我们没有办法预期位置③的断言每次都成功,因为我们没法保证操作①每次都在操作②之前完成。 这里有两个主要的原因: 编译器可能会对没有依赖的语句进行优化,重排他们的执行顺序 CPU在执行字节码的时候,会对没有依赖关系的语句重排执行顺序 显然在上面的例子中,操作②就有可能被重排到操作①之前,在这种情况下我们在线程2就没法观测到正确的结果,从而导致位置③的断言失败。 考虑这样的一种情况: asm L1: LDR R1, [R0] L2: ADD R2, R1, R1 L3: ADD R4, R3, R3 在按序执行的情况下,我们预期的顺序应该是: L1->L2->L3 然而我们可以很容易的发现,语句3 和语句1 是没有依赖关系的,而语句2可以会依赖于语句1的执行结果,所以CPU经过乱序,并且可能将这三个操作发送到两个不同的CPU单元上,并且得到另一种执行顺序: L1->L2 L3 再就是现在的多核CPU带来的可能缓存不一致的问题,在一个CPU核心上后写入的数据在其他的地方未必是后写入的。所以就会出现我们最上面的例子中,我们尝试使用一个标记位用来标记其他的数据是否已经准备好了,然后我们可能会在另一个核心上来判断这个标志位来决定所需要的数据是否已经准备就位。这样的操作的风险就在于,可能在被乱序执行的情况下,标志位被先写入了,然后才开始准备数据,这样在另一个核心观测就会得到不一样的、错误的结果。所以我们就必须在我们的代码中做出一些保护机制。 在C++11之前,我们有一种普遍的用法,就是内存屏障。而在C++11中,我们有了另一个选择,就是atomic。 3 atomic in C++11 atomic 是在 C++11 中被加入了标准库的,这个库提供了针对于布尔、整数和指针类型的原子操作。原子操作意味着不可分的最小执行单位,一个原子操作要么成功,要么失败,是不会被线程的切换多打断的执行片段。对于在不同的线程上访问原子类型上操作是well-defined的,是不具有数据竞争的。 3.1 模板类 atomic 模板类 atomic 是整个库的核心,标准库中提供了针对布尔类型、整数类型和指针类型的特化,除此之外的情况请保证用于特化模板的类型时一个平凡的(trivial)类型。 在原子类上,显然有两个基础操作: c++ void store(T, memory_order = memory_order_seq_cst) volatile noexcept; void store(T, memory_order = memory_order_seq_cst) noexcept; T load(memory_order = memory_order_seq_cst) const volatile noexcept; T load(memory_order = memory_order_seq_cst) const noexcept; 用于更新原子对象当前值的 store 方法和读取原子对象当前值的 load 方法。对于 store 方法,指定的内存顺序必须是 std::memory_order_relaxed、std::memory_order_release 或 std::memory_order_seq_cst其中的一个,指定为其他的内存顺序都是未定义行为;对于load方法,指定的内存顺序必须是 std::memory_order_relaxed、std::memory_order_consume、std::memory_order_acquire或std::memory_order_seq_cst其中的一个,其他的内存顺序同样都是未定义行为。 还有一个操作,原子的以新值替换旧值并返回旧值。 c++ T exchange( T desired, std::memory_order order = std::memory_order_seq_cst ) noexcept; T exchange( T desired, std::memory_order order = std::memory_order_seq_cst ) volatile noexcept; 这是一个读-修改-写的操作,类似的还有test-and-set,fetch-and-add,compare-and-swap。 4 Memory Model C++中提供了六种内存模型,其中的一些通常会成对出现。 memory_order_relaxed:对操作的上下文没有要求,仅要求当前操作的原子性 memory_order_consume:当前的加载操作,在其影响的内存位置上进行 消费:当前线程中依赖于该值读或写的操作不能被重排到该操作之前;在其他线程中,该值所依赖的变量的写入都可以被当前线程正确的观测到 memory_order_acquire:当前的加载操作,在其影响的内存位置上进行 获取:当前线程的读或写都不能重排到该操作之前;在其他线程中的所有位于该操作之前的读或写都可以被当前线程正确的观测到 memory_order_release:当前的存储操作,在其影响的内存位置上进行 释放:当前线程的读或写都不能重排到该操作之前;在其他线程中的所有位于该操作之前的写都可以被当前线程正确的观测到 memory_order_acq_rel:当前的加载或是存储操作,既在其影响的内存位置上进行 获取 也进行 释放 memory_order_seq_cst:当前的加载操作在其影响的内存位置进行 获取,存储操作进行 释放,读-修改-写操作进行 获取 和 释放 4.1 memory_order_relaxed 在这个内存模型中,不要求操作在访问同样内存时候的操作顺序,只保证了原子性和修改的一致性。考虑下面的例子,对于初值为0的两个原子量x和y: c++ // thread 1 r1 = y.load(memory_order_relaxed); // 1 x.store(r1, memory_order_relaxed); // 2 // thread 2 r2 = x.load(memory_order_relaxed); // 3 y.store(42, memory_order_relaxed); // 4 这里是允许出现x和y同时等于42的情况,因为我们即使知道操作①先于操作②,操作③先于操作④;但是我们没有约束操作④不能优先出现于操作①。所以我们可以观测到任何可能的结果。 4.2 memory_order_acquire && memory_order_consume 在这个顺序模型中,存储操作 释放 ;加载操作 消费 。如果线程1中的存储操作使用了 释放 标记;而线程2中的加载操作使用了 消费 标记。那么在线程1中的存储操作所依赖的所有内存写入都对在线程2中都可以被正确的观测到。这种同步仅仅建立在存储和加载的两个线程之间,对其他线程无效。 可以使用std::kill_dependency来消除从带有消费标记的加载操作开始的依赖树,不会讲依赖带入返回值。这个操作可以避免依赖链在离开函数作用域时,不必要的memory_order_acquire栅栏。 4.3 memory_order_acquire && memory_order_release 在这个顺序模型中,存储操作 释放;加载操作 获取。如果线程1中的存储操作使用了 释放 标记;而线程2中的加载操作使用了 获取 标记。那么在线程1中,所以先于存储操作的内存写入在线程2中都可以被正确的观测到。这种同步仅建立在存储和加载的两个线程之间,对其他的线程无效。 所以考虑最开始的代码,如果我们将变量a改为atomic<int>,并且使用 release-acquire 的内存模型,就可以保证断言③的绝对正确。 c++ atomic<int> a; int b; 线程1 c++ b = 1; // 1 a.store(1, std::memory_order_release); // 2 线程2: c++ int a = 0, b = 0; while (a.load(std::memory_order_acquire) == 0); assert(b == 1); // 3 4.4 memory_order_seq_cst 除了在进行 释放 和 获取 操作外,还会的所有持有此标记的操作建立一个单独全序(single total modification order)。这个表示每个标记了memory_order_seq_cst的操作,都可以观测到在其之前发生的标记有memory_order_seq_cst;并且可能观测到在其之前的,未标记为memory_order_seq_cst的操作。 c++ #include <thread> #include <atomic> #include <cassert> std::atomic<bool> x = {false}; std::atomic<bool> y = {false}; std::atomic<int> z = {0}; void write_x() { x.store(true, std::memory_order_seq_cst); } void write_y() { y.store(true, std::memory_order_seq_cst); } void read_x_then_y() { while (!x.load(std::memory_order_seq_cst)); if (y.load(std::memory_order_seq_cst)) { ++z; } } void read_y_then_x() { while (!y.load(std::memory_order_seq_cst)); if (x.load(std::memory_order_seq_cst)) { ++z; } } int main(){ std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); a.join(); b.join(); c.join(); d.join(); assert(z.load() != 0); // 1 } 上面例子中操作①处的断言绝不可能失败。 使用此序列顺序在多核模式下要求完全内存栅栏的CPU指令,这可能会成为性能的瓶颈,因为它将其受影响的内存的影响传播到了每个核心。

2018/3/19
articleCard.readMore

在C++17中的部分新特性

C++17已经发布了有一些时候,并且很多的编译器已经完成了对C++17的支持,那对于C++17中的新特性,我也好奇的玩了一些,其中的几个新特性特别吸引我的注意。 15 if-init 15.1 Grammar if ( init-statement condition ) init-statement 可以是: 表达式语句(可以是空语句,仅;) 声明语句 15.2 Thinking if-init 这个 feature 尝试着让我们的代码更可读和干净,并且可以帮助我们更好的控制对象的生命周期。在从前的 if 语句中,我们经常会做类似的事情: c++ void foo(int a) { auto iter = a_map.find(a); if (iter != a_map.end()) { // do_something } else { // do_something_else } // do_something_next } 这个叫做iter的变量其实我们除了用于这个if判断和语句块中的操作之外,我们可能再也不会用到它。它的生命周期其实无形的被延长到了整个函数的结束。就像我们在for语句中做的一样,我们为什么不把这个对象的生命周期限定在一个if语句块中呢?所以就有了 if-init 这个 feature。 c++ void foo(int a) { if (auto iter = a_map.find(a); iter != a_map.end()) { // do_something } else { // do_something_else } // do_something_next } 这里的写法就像在for语句中的那样自然,我们在一个if中初始化一个对象,并且使用它,当离开if语句的时候,这个对象就被销毁了。 一些更多的用法: c++ // with temporary read buffer if (char buf[64]; std::fgets(buf, 64, stdin)) { m[0] += buf; } // with lock if (std::lock_guard<std::mutex> lock(mx); shared_flag) { unsafe_ping(); shared_flag = false; } // with temporary input param if (int s; int count = ReadBytesWithSignal(&s)) { publish(count); raise(s); } 16 structured-bingds 16.1 Grammar attr(optional) cv-auto ref-operator(optional) [ indentifier-list ] = expression ; attr(optional) cv-auto ref-operator(optional) [ indentifier-list ] { expression }; attr(optional) cv-auto ref-operator(optional) [ indentifier-list ] ( expression ); 这里在绑定时候的基本规则是:(这里用 Expression 来表示 expression 表达式的类型) 如果 Expression 是数组类型,则一次绑定到数组元素 如果 Expression 是非联合体类型,且std::tuple_size<Expression>是完整类型,则使用类tuple绑定 如果 Expression 是非联合体类型,且std::tuple_size<Expression>不是完整类型,则以此绑定到类的公开数据成员 16.1.1 Case 1:Binding an array 每个 indentifier-list 中的标识符绑定到数组的每一个元素,标识符数量必须和数组大小一致。 c++ int a[2] = {1,2}; auto [x,y] = a; auto& [xr, yr] = a; 16.1.2 Case 2: Binding a tuple-like type std::tuple_size<Expression>::value必须是一个合法的编译时期常量,并且标识符的数量和std::tuple_size<Expression>::value的值相等。 在 ref-operator 为&或者&&时: 对于每一个标识符,若其对应的初始化表达式是左值,则为左值引用;若为右值则为右值引用。 每个标识符对应的初始化表达式使用如下规则: expression.get<i>(),表达式包含了一个如此的声明 get<i>(expression),仅按照 ADL1 规则查找对应的声明(在这种使用中,如果 expression 对象是一个左值,那么传入的参数也为左值;如果 expression 对象是一个右值,那么传入的对象也为右值) c++ float x{}; char y{}; int z{}; const auto& [a,b,c] = std::tuple<float&,char&&,int>(x,std::move(y),z); 16.1.3 Case 3: Binding to public data members 每个 Expression 的非静态公开成员变量都会被依次绑定,并且标识符个数要和非静态公开成员变量个数相等。 所以的非静态公开成员都应该是无歧义的,且不能有任何的匿名联合体成员。 标识符最后的类型的 CV限定符会合并其类成员变量的类型中的CV限定符和声明中的的CV限定符。 c++ struct S { int x1 : 2; volatile double y1; }; S f(); const auto [x, y] = f(); 16.2 Thinking structured-binding 带给我们在书写上方便的同时也带来了相应的更大的心智负担,它让我们不仅要关注一个变量的类型,我们还要关注它所指向的对象的类型。因为在这里创建的不是一个引用,也不是对象的拷贝,而是一个既存对象的别名。 比如下面这个例子: c++ BarBar foo(Bar bar) { // do_something return bar.bar; } 如果我们如此明显的书写的话,我们都知道,返回一个 sub-object 是不能触发 RVO2 的。那么我们如果用了结构化绑定的方式之后呢? c++ BarBar foo(Bar bar) { auto [..., b, ...] = bar; // do_something return b; } 很遗憾的是,这里依旧不能触发 RVO 的,因为这里的b是一个对象的别名,既不是引用也不是什么别的,它依旧会被认为是一个 sub-object。 17 if-init with structured-bindings 将 if-init 和 structured-bindings 可以帮助我们在很多地方缩减我们的代码: c++ if (auto [iter, inserted] = m.insert({"foo", "bar"}); inserted) { // do_something } else { // do_other_things } 18 if-constexpr if-constexpr 可以帮助我们简化原本很多需要用 SFINAE 来实现的代码,用来模板中使用哪一部分的实现。 18.1 Sample 1 18.1.1 Before C++17 c++ #include <type_traits> template<typename T> auto get_value_impl(T t, std::false_type) { return t; } template<typename T> auto get_value_impl(T t, std::true_type) { return *t; } template<typename T> auto get_value(T t) { return get_value_impl(t, std::is_pointer<T>()); } 18.1.2 After C++17 c++ template <typename T> auto get_value(T t) { if constexpr (std::is_pointer_v<T>) return *t; else return t; } 18.2 Sample 2 18.2.1 Before C++17 c++ template<int N> constexpr int fibonacci() {return fibonacci<N-1>() + fibonacci<N-2>(); } template<> constexpr int fibonacci<1>() { return 1; } template<> constexpr int fibonacci<0>() { return 0; } 18.2.2 After C++17 c++ template<int N> constexpr int fibonacci() { if constexpr (N>=2) return fibonacci<N-1>() + fibonacci<N-2>(); else return N; } 18.3 Summary if-constexpr 的出现我感觉不是为了解决什么特别的问题,而是为了简化我们的代码,让我们在写的时候可以更自然,更符合直觉。 19 fold expression 19.1 Grammar ( pack op … ) ( … op pack ) ( pack op … op init ) ( init op … op pack ) 上面四个分别对应了一元右折叠、一元左折叠、二元右折叠、二元左折叠。 19.2 Explain 上面的四种折叠,分别会被展开成: text \text{一元右折叠:} E_1 \ \text{op}\ (\dots\ \text{op}\ (E_{N-1}\ \text{op}\ E_N))\\ \text{一元左折叠:} ((E_1\ \text{op}\ E_2)\ \text{op}\ \dots)\ \text{op}\ E_N)\\ \text{二元右折叠:} E_1 \ \text{op}\ (\dots\ \text{op}\ (E_{N-1}\ \text{op}\ (E_N\ \text{op}\ I)))\\ \text{二元左折叠:} (((I\ \text{op}\ E_1)\ \text{op}\ E_2)\ \text{op}\ \dots)\ \text{op}\ E_N) 在使用二元折叠的时候,注意两个运算符必须是一样的,并且 init 如果是一个表达式的话,优先级必须低于 op,如果一定要高于的话,可以用括号括起来。 19.3 Example 端序交换: c++ template<class T, std::size_t... N> constexpr T bswap_impl(T i, std::index_sequence<N...>) { return (((i >> N*CHAR_BIT & std::uint8_t(-1)) << (sizeof(T)-1-N)*CHAR_BIT) | ...); } template<class T, class U = std::make_unsigned_t<T>> constexpr U bswap(T i) { return bswap_impl<U>(i, std::make_index_sequence<sizeof(T)>{}); } 19.4 Thinking 折叠表达式是在参数包的展开上面的又一次进步和改进,弥补了原本的参数包展开不易计算的问题。 20 noexcept noexcept 成为了类型系统的一部分,这也是C++17对以前代码的唯一影响,可能会导致以前的代码不能通过编译。 void f(); 和 void f() noexcept; 不再被认为是同一个类型,所以可能要为从前的实现提供额外的模板,或者提供额外的重载,再或者可以在模板中使用C++17中的新特性指定模板类型推导(template type deduction guide)。 21 template type deduction 为了实例化一个类模板,我们必须清楚的知道每个模板的类型,并且显示的写出他们,但是很多时候这是不必要的。 21.1 Thinking 这个新特性看起来似乎并没有那么吸引我:在类成员定义的时候,我没法使用到这个特性,可是这是我使用模板类最多的地方;在其他的很多地方,我似乎又可以使用auto来替代写出一个变量的类型来。 References: http://en.cppreference.com/w/cpp/language/if http://en.cppreference.com/w/cpp/language/structured_binding http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0144r0.pdf ADL: Argument-dependent lookup ↩︎ RVO: Return Value Optimization ↩︎

2018/2/25
articleCard.readMore

Const Reference of Pointer

问题起源: 在子类中实现一个模板父类的纯虚函数的时候,不能正确的通过编译。 c++ template<typename T> struct Fuck { virtual void shit(const T&) = 0; } shit函数接受一个常量引用,当我们使用一个指针类型(A*)来实例化这个模板类的时候,函数shit的类型就应该是: c++ void shit(const T&) = 0; <value T = A*> 当我尝试用下面这样的表示来实现这个函数的时候发生了编译错误: c++ struct FuckImpl : Fuck<A*> { void shit(const A*&) override; } 这里正确的写法应该是: c++ struct FuckImpl : Fuck<A*> { void shit(A* const&) override; }; 这个问题大概由于const修饰符的结合性的问题,在前一种写法中const并没有修饰后面的引用,而是由于结合性的原因修饰了前面的指针。所以后一种写法中,const明确的修饰了后面引用。提供了正确的类型。 额外的吐槽:这里就要吐槽g++的报错了,我用clang编译的时候就给出了正确的表达式写法,只要抄上去就好了。2333

2018/1/17
articleCard.readMore