标签: C++

  • Effective STL 读后感

    这本书的全名叫《Effective STL — 50条有效使用STL的经验》,书其实已经有点历史了,Meyers在2001年时给它写的前言,如今已经2022了。

    正如上面所说,因为这本书已经有些年头了,如果你已经是C++11、14、17、20的使用者,可以把第6章函数子、函数子类、函数及其他的内容快速带过看,因为std::functional以及lambda函数已经让C++函数类支持有了显著提升。

    简略写一下对我而言有用的一些经验:

    第12条:切勿对STL容器的线程安全性有不切实际的依赖
    1. 多线程读是安全的
    2. 多线程对不同容器做写入是安全的
    第14条:使用reserve来避免不必要的重新分配

    尽管我认为这应该是C++ Engineer的常识了,不过还是有好多小朋友不知道这个点。

    第23条:考虑用排序的vector代替关联容器

    可以详细看一下。我觉得根本问题是map类的容器对cache不是很友好。连续内存访问总是很香的。

    第25条:熟悉非标准的散列容器

    写书的时候还没有stl::unordered_map。不过即便放2022年,我也觉得可以看看STL之外的散列容器实现,比如robin-hood-hashing。曾经用过比较坑的一个容器是boost::flat_map,千万别认为它是O(1)的实现。

    第31条:了解各种与排序有关的选择

    <algorithm>头文件里有很多宝藏,在选择自己实现之前,最好看看STL有没有帮你写好了轮子。

    第32条:如果确实需要删除元素,则需要在remove这一类算法之后调用erase

    曾经有个说法是std::move does not actually move anything, 对于std::remove也有点类似。

    第44条:容器的成员函数优先于同名的算法

    如题。

    第47条:避免产生“直写型”(write-only)的代码

    工程上非常中肯的一条经验。如果没有特殊原因(如及其苛刻的性能要求),尽量不要写出非常难懂的代码,不然几个月乃至几年之后你来调试自己写的代码时也会骂娘。

    代码被阅读的次数远远大于它被编写的次数

  • Boost Python的C++对象, Pickle支持及其原理

    默认用boost python包裹的C++对象是不支持pickle的,如果要用pickle.dumps(obj)的话那会提示错误

    Pickling of "xxx" instances is not enabled.

    这边吐槽一下在最新的代码里,给的reference链接其实还是不可用的。真正正确的是https://www.boost.org/doc/libs/1_74_0/libs/python/doc/html/reference/topics/pickle_support.html

    让你的class支持Pickle协议

    若要让你的C++ Class支持Pickle协议,比较“正统”的方法是利用boost提供的boost::python::pickle_suite. 拿代码说话:

    struct world_t {
        world_t(const string& country) { ... }
    };
    
    struct world_pickle_suite : boost::python::pickle_suite
      {
        static
        boost::python::tuple
        getinitargs(const world_t& w)
        {
          // [可选实现] 返回一个boost::python::tuple元组,其值被用来构造
          // 如果一个类的构造函数**不需要参数**的话,可以不用重载这个方法。
          return boost::python::make_tuple(w.country());
        }
    
        static
        boost::python::tuple
        getstate(const world_t& w)
        {
          // [可选实现] 如果对象的构造函数并不能完全恢复对象的状态,
          // 那么要用此函数返回其状态值
        }
    
        static
        void
        setstate(world_t& w, boost::python::tuple state)
        {
          // [可选实现] 如果对象的构造函数并不能完全恢复对象的状态,
          // 那么要用此函数把state里的状态恢复到w
        }
      };
    
    boost::python::class_<world_t>("world", args<const std::string&>())
      // 定义world_t的boost python包装类
      .def_pickle(world_pickle_suite());
      // ...
    

    需要注意的是,如果在suite里定义了getstate/setstate并且这个类的__dict__属性非空,boost是会报错的。一种可能的情况是你包装的类是一个子类,这时候还需要自己实现__getstate_manages_dict__属性。这边不赘述,可参考这里

    原理简释

    总所周知,Python3里若要在自定义的类里实现可Pickle,至少需要:

    • 它的__dict__属性是可pickle的,或;
    • 调用__getstate__()拿到的返回值是可pickle的,随后会调用__setstate__()方法在unpickle的时候恢复状态.
      但在这些函数的背后,再稍微底层一点其实是通过__reduce__()方法实现的(更严格一点,它会先去寻找__reduce_ex__()是否可用)。这个方法简单来说就是返回一个tuple, 这个tuple有从构建对象到恢复状态所需的各种元素.

    In fact, these methods are part of the copy protocol which implements the __reduce__() special method.
    https://docs.python.org/3/library/pickle.html

    所以boost::python其实自己实现了一个对象的__reduce__()方法,在src/object/pickle_support.cpp里(源文件在这)。主要做了几件事

    1. 拿到当前对象的__class__属性(也就是class object);
    2. 检查对象是否有__safe_for_unpickling__属性。如果没有的话,就是本文最早提到的报错了;
    3. 检查对象是否有__getinitargs__()方法,若有则取值。没有的话,unpickle的时候按照无参数构造来处理;
    4. 检查对象是否有__getstate__()方法,若有则取值;
    5. 检查对象是否有__dict__属性,若有则会检查是否有__getstate_manages_dict__属性并获取这两个属性的值。
    6. 返回tuple(),内容依次为1-5里拿到的值。

    可以看到,这个__reduce__()方法是遵循了Python里的object.__reduce__()协定的。当然了,如果某些使用者觉得继承一个suite类都觉得麻烦或者达不到自己的需求,也可以通过其他手段完成,只要记得自己整一个__reduce__()方法,只需满足Python的协定即可.

    class_obj.attr("__reduce__") = boost::python::object(/*YOUR REDUCE FUNCTION*/);

    再深一点~

    如果你跟我一样无聊,那就来看看cpython自己想要__reduce__()的时候是怎么用到__getstate__()的吧.

    cpython/Objects/typeobject.c这边有个函数叫

    static PyObject* _common_reduce(PyObject *self, int proto)

    ,在pickle protocol>2时会调用`static PyObject *
    reduce_newobj(PyObject *obj)`方法,这个方法是Python里大多数默认对象的默认__reduce__逻辑。其中有一句

    state = _PyObject_GetState(obj,
                    !hasargs && !PyList_Check(obj) && !PyDict_Check(obj));

    _PyObject_GetState里大致是从__dict__或者__slots__属性去获取对象的状态。

    这边再往上一些还能看到有调用_PyObject_GetNewArguments()的,里面的逻辑就会去拿__getnewargs_ex__或是__getnewargs__属性的值作为__new__一个新对象时候的传入参数。这两个函数是Pickle协议文档里介绍的四个推荐覆盖的“高阶”函数之一。与之对应,Python并不希望一般人去自己写一个__reduce__()出来.

    看到这里,再回想一下boost::python,pickle_suite里面的getinitargs()getstate()就分别对应到__getnewargs__()__getstate__()

  • Support parallel XZ decompression for unix (7zip LZMA SDK based, C/C++)

    为unix平台增加XZ多线程解压缩支持(基于7zip LZMA SDK, C/C++)

    Note

    This post has nothing to do with the pixz project. I am talking about decompressing the original xz archive using 7-zip’s LZMA SDK under unix environment.

    Background

    Originally the 7zip’s LZMA SDK (version 19.00) only covers parallel xz decompression for Windows systems. This post shows the C code that adds support for lib pthread, i.e. unix systems.
    Compiling with C++ using the C library should also work, I have tested it on my own box.

    Little Details

    Actually the original writer has completed all the necessary abstraction for the multi-threading pipeline. All I have done is adding some macros and pthread equivalents to Windows threading model.

    Usage

    Replace lzma/C/... with below files. The new code should work on both Windows and Unix systems.

    Git Repo

    See https://github.com/idailylife/lzma-xz-parallel

    Source Code

    Threads.h

    /* Threads.h -- multithreading library
    2017-06-18 : Igor Pavlov : Public domain */
    
    #ifndef __7Z_THREADS_H
    #define __7Z_THREADS_H
    
    #ifdef _WIN32
    #include <windows.h>
    #else
    #include <pthread.h>
    #endif
    
    #include "7zTypes.h"
    
    EXTERN_C_BEGIN
    
    WRes HandlePtr_Close(HANDLE *h);
    WRes Handle_WaitObject(HANDLE h);
    
    #ifdef _WIN32
    typedef HANDLE CThread;
    #define Thread_Construct(p) *(p) = NULL
    #define Thread_WasCreated(p) (*(p) != NULL)
    #define Thread_Close(p) HandlePtr_Close(p)
    #define Thread_Wait(p) Handle_WaitObject(*(p))
    #else
    typedef void* LPVOID;
    typedef pthread_t* CThread;
    #define Thread_Construct(p) *(p) = NULL
    #define Thread_WasCreated(p) (*(p) != NULL)
    #define Thread_Close(p) HandleThread_Close(*(p))
    #define Thread_Wait(p) HandleThread_Join(*(p))
    WRes HandleThread_Close(pthread_t* th);
    WRes HandleThread_Join(pthread_t* th);
    
    #endif
    
    
    typedef
    #ifdef UNDER_CE
      DWORD
    #else
      unsigned
    #endif
      THREAD_FUNC_RET_TYPE;
    
    #define THREAD_FUNC_CALL_TYPE MY_STD_CALL
    #define THREAD_FUNC_DECL THREAD_FUNC_RET_TYPE THREAD_FUNC_CALL_TYPE
    typedef THREAD_FUNC_RET_TYPE (THREAD_FUNC_CALL_TYPE * THREAD_FUNC_TYPE)(void *);
    WRes Thread_Create(CThread *p, THREAD_FUNC_TYPE func, LPVOID param);
    
    #ifdef _WIN32
    
    typedef HANDLE CEvent;
    typedef CEvent CAutoResetEvent;
    typedef CEvent CManualResetEvent;
    #define Event_Construct(p) *(p) = NULL
    #define Event_IsCreated(p) (*(p) != NULL)
    #define Event_Close(p) HandlePtr_Close(p)
    #define Event_Wait(p) Handle_WaitObject(*(p))
    WRes Event_Set(CEvent *p);
    WRes Event_Reset(CEvent *p);
    WRes ManualResetEvent_Create(CManualResetEvent *p, int signaled); // not used
    WRes ManualResetEvent_CreateNotSignaled(CManualResetEvent *p); // not used
    WRes AutoResetEvent_Create(CAutoResetEvent *p, int signaled);
    WRes AutoResetEvent_CreateNotSignaled(CAutoResetEvent *p);
    
    #else
    typedef struct {
      bool state;
      pthread_mutex_t mutex;
      pthread_cond_t cond;
    } event_t;
    
    typedef event_t* CEvent;
    typedef CEvent CAutoResetEvent;
    #define Event_Construct(p) *(p) = NULL
    #define Event_IsCreated(p) (*(p) != NULL)
    
    WRes Event_Close(CEvent* p);
    WRes Event_Set(CEvent *p);
    WRes Event_Reset(CEvent *p);
    WRes Event_Wait(CEvent* p);
    WRes AutoResetEvent_CreateNotSignaled(CAutoResetEvent* p);
    
    #endif
    
    // CSemaphore is not used for decoding
    #ifdef _WIN32
    typedef HANDLE CSemaphore;
    #define Semaphore_Construct(p) *(p) = NULL
    #define Semaphore_IsCreated(p) (*(p) != NULL)
    #define Semaphore_Close(p) HandlePtr_Close(p)
    #define Semaphore_Wait(p) Handle_WaitObject(*(p))
    WRes Semaphore_Create(CSemaphore *p, UInt32 initCount, UInt32 maxCount);
    WRes Semaphore_ReleaseN(CSemaphore *p, UInt32 num);
    WRes Semaphore_Release1(CSemaphore *p);
    #endif
    
    #ifdef _WIN32
    
    typedef CRITICAL_SECTION CCriticalSection;
    WRes CriticalSection_Init(CCriticalSection *p);
    #define CriticalSection_Delete(p) DeleteCriticalSection(p)
    #define CriticalSection_Enter(p) EnterCriticalSection(p)
    #define CriticalSection_Leave(p) LeaveCriticalSection(p)
    
    #else
    /// use mutex instead
    typedef pthread_mutex_t* CCriticalSection
    WRes CriticalSection_Init(CCriticalSection *p);
    WRes CriticalSection_Delete(CCriticalSection *p);
    WRes CriticalSection_Enter(CCriticalSection *p);
    WRes CriticalSection_Leave(CCriticalSection *p);
    
    #endif
    EXTERN_C_END
    
    #endif
    

    Threads.c

    /* Threads.c -- multithreading library
    2017-06-26 : Igor Pavlov : Public domain */
    
    #include "Precomp.h"
    
    #ifdef _WIN32
      #ifndef UNDER_CE
      #include <process.h>
      #endif
    #endif
    
    #include "Threads.h"
    
    #ifdef _WIN32
    static WRes GetError()
    {
      DWORD res = GetLastError();
      return res ? (WRes)res : 1;
    }
    
    static WRes HandleToWRes(HANDLE h) { return (h != NULL) ? 0 : GetError(); }
    static WRes BOOLToWRes(BOOL v) { return v ? 0 : GetError(); }
    
    WRes HandlePtr_Close(HANDLE *p)
    {
      if (*p != NULL)
      {
        if (!CloseHandle(*p))
          return GetError();
        *p = NULL;
      }
      return 0;
    }
    
    WRes Handle_WaitObject(HANDLE h) { return (WRes)WaitForSingleObject(h, INFINITE); }
    
    #else
    /// unix specific functions
    
    WRes HandleThread_Close(pthread_t* th) {
      free(th);
      th = NULL;
      return 0;
    }
    
    WRes HandleThread_Join(pthread_t* th) {
      return pthread_join(*th, NULL);
    }
    
    #endif
    
    
    #ifdef _WIN32
    WRes Thread_Create(CThread *p, THREAD_FUNC_TYPE func, LPVOID param)
    {
      /* Windows Me/98/95: threadId parameter may not be NULL in _beginthreadex/CreateThread functions */
      
      #ifdef UNDER_CE
      
      DWORD threadId;
      *p = CreateThread(0, 0, func, param, 0, &threadId);
    
      #else
    
      unsigned threadId;
      *p = (HANDLE)_beginthreadex(NULL, 0, func, param, 0, &threadId);
       
      #endif
    
      /* maybe we must use errno here, but probably GetLastError() is also OK. */
      return HandleToWRes(*p);
    }
    #else
    pthread_attr_t g_th_attrs[64]; //NOTE: maximum of 64 threads
    size_t g_th_index = 0;
    
    WRes Thread_Create(CThread *p, THREAD_FUNC_TYPE func, LPVOID param)
    {
      *p = malloc(sizeof(pthread_t));
      pthread_t* th = *p;
      int ret = pthread_attr_init(&(g_th_attrs[g_th_index]));
      assert(ret==0);
      ret = pthread_create(th, &(g_th_attrs[g_th_index]), func, param);
      g_th_index++;
      return ret;
    }
    #endif
    
    
    #ifdef _WIN32
    static WRes Event_Create(CEvent *p, BOOL manualReset, int signaled)
    {
      *p = CreateEvent(NULL, manualReset, (signaled ? TRUE : FALSE), NULL);
      return HandleToWRes(*p);
    }
    
    WRes Event_Set(CEvent *p) { return BOOLToWRes(SetEvent(*p)); }
    WRes Event_Reset(CEvent *p) { return BOOLToWRes(ResetEvent(*p)); }
    
    WRes ManualResetEvent_Create(CManualResetEvent *p, int signaled) { return Event_Create(p, TRUE, signaled); }
    WRes AutoResetEvent_Create(CAutoResetEvent *p, int signaled) { return Event_Create(p, FALSE, signaled); }
    WRes ManualResetEvent_CreateNotSignaled(CManualResetEvent *p) { return ManualResetEvent_Create(p, 0); }
    WRes AutoResetEvent_CreateNotSignaled(CAutoResetEvent *p) { return AutoResetEvent_Create(p, 0); }
    
    #else 
    ///unix
    
    WRes Event_Close(CEvent* p) {
      if (!p || !(*p))
        return 0;
      event_t* evt = *p;
      pthread_cond_destroy(&evt->cond);
      pthread_mutex_destroy(&evt->mutex);
      free(evt);
      *p = NULL;
    }
    
    
    WRes Event_Set(CEvent *p) {
      event_t* evt = *p;
      if (pthread_mutex_lock(&evt->mutex) != 0) {
        return 1;
      }
      evt->state = true;
    
      if (evt->manual_reset) {
        if (pthread_cond_broadcast(&evt->cond)) {
          return 1;
        }
      } else {
        if (pthread_cond_signal(&evn->cond)) {
          return 1;
        }
      }
    
      if (pthread_mutex_unlock(&evt->mutex)) {
        return 1;
      }
    
      return 0;
    }
    
    WRes Event_Reset(CEvent* p) {
      event_t* evt = *p;
      if (pthread_mutex_lock(&evt->mutex)) {
        return 1;
      }
    
      evt->state = false;
    
      if (pthread_mutex_unlock(&evt->mutex)) {
        return 1;
      }
    
      return 0;
    }
    
    WRes Event_Wait(CEvent* p) {
      event_t* evt = *p;
      if (pthread_mutex_lock(&evt->mutex)) {
        return 1;
      }
    
      while (!evt->state) {
        if (pthread_cond_wait(&evt->cond, &evt->mutex)) {
          pthread_mutex_unlock(&evt->mutex);
          return 1;
        }
      }
    
      evt->state = false;
      if (pthread_mutex_unlock(&evt->mutex)) {
        return 1;
      }
      return 0;
    }
    
    WRes AutoResetEvent_CreateNotSignaled(CAutoResetEvent *p) {
      *p = malloc(sizeof(event_t));
      memset(evt, 0, sizeof(event_t));
      evt->state = false;
      evt->manual_reset = false;
      if (pthread_mutex_init(&evt->mutex, NULL)) {
        return 1;
      }
      if (pthread_cond_init(&evt->cond, NULL)) {
        return 1;
      }
      return 0;
    }
    
    #endif
    
    
    #ifdef _WIN32
    
    WRes Semaphore_Create(CSemaphore *p, UInt32 initCount, UInt32 maxCount)
    {
      *p = CreateSemaphore(NULL, (LONG)initCount, (LONG)maxCount, NULL);
      return HandleToWRes(*p);
    }
    
    static WRes Semaphore_Release(CSemaphore *p, LONG releaseCount, LONG *previousCount)
      { return BOOLToWRes(ReleaseSemaphore(*p, releaseCount, previousCount)); }
    WRes Semaphore_ReleaseN(CSemaphore *p, UInt32 num)
      { return Semaphore_Release(p, (LONG)num, NULL); }
    WRes Semaphore_Release1(CSemaphore *p) { return Semaphore_ReleaseN(p, 1); }
    
    #endif
    
    
    #ifdef _WIN32
    WRes CriticalSection_Init(CCriticalSection *p)
    {
      /* InitializeCriticalSection can raise only STATUS_NO_MEMORY exception */
      #ifdef _MSC_VER
      __try
      #endif
      {
        InitializeCriticalSection(p);
        /* InitializeCriticalSectionAndSpinCount(p, 0); */
      }
      #ifdef _MSC_VER
      __except (EXCEPTION_EXECUTE_HANDLER) { return 1; }
      #endif
      return 0;
    }
    
    #else
    WRes CriticalSection_Init(CCriticalSection *p) {
      *p = malloc(sizeof(pthread_mutex_t));
      if (pthread_mutex_init(*p, NULL)) {
        return 1;
      }
      return 0;
    }
    
    WRes CriticalSection_Delete(CCriticalSection *p) {
      pthread_mutex_t* mtx = *p;
      return pthread_mutex_destroy(mtx);
    }
    
    WRes CriticalSection_Enter(CCriticalSection *p) {
      if (pthread_mutex_lock(&evt->mutex)) {
        return 1;
      }
      return 0;
    }
    
    WRes CriticalSection_Leave(CCriticalSection *p) {
      if (pthread_mutex_unlock(&evt->mutex)) {
        return 1;
      }
      return 0;
    }
    
    #endif
    

    XzDec.c

    add the following macro

    #ifndef _WIN32
    #define S_OK 0x00000000
    #define E_FAIL 0x80004005
    #endif

    MtDec.c

    replace function ThreadFUnc1 with:

    static THREAD_FUNC_RET_TYPE THREAD_FUNC_CALL_TYPE ThreadFunc1(void* pp)
    {
      static int g_ok_stat = 0x0;
      static int g_err_stat = 0x80004005;
      WRes res;
      CMtDecThread *t = (CMtDecThread*)pp;
      CMtDec *p;
    
      res = ThreadFunc2(t);
      p = t->mtDec;
      if (res == 0) {
    #ifdef _WIN32
        return p->exitThreadWRes;
    #else
        if (p->exitThreadWRes) { return &g_err_stat; }
        else { return &g_ok_stat; }
    #endif
      }
      {
        // it's unexpected situation for some threading function error
        if (p->exitThreadWRes == 0)
          p->exitThreadWRes = res;
        PRF(printf("\nthread exit error = %d\n", res));
        p->exitThread = True;
        Event_Set(&p->threads[0].canRead);
        Event_Set(&p->threads[0].canWrite);
        MtProgress_SetError(&p->mtProgress, MY_SRes_HRESULT_FROM_WRes(res));
      }
    #ifdef _WIN32
        return res;
    #else
        return &g_err_stat;
    #endif
    }
  • std::future 与 std::promise 简单使用、简单原理

    C++11有了一些关于线程的模型,在此之前C++里可是各自为政的,各种线程库各种神奇用法。其中有两个好玩的东西就是std::promise<T>std::future<T>,下文书中形容它们像是个“虫洞”。

    • std::future是虫洞的出口:一个未来对象的“获取器”,在未来的某一刻它能返回一个有用的值,但现在还没有…
    • std::promise是虫洞的入口:我们要保证在未来的某一刻给一个值进去,但不是现在…

    Basic usage

    一般来说这俩是成对使用的,看个代码:

    #include <cassert>
    #include <chrono>
    #include <future>
    #include <iostream>
    #include <string>
    #include <thread>
    
    using namespace std::chrono_literals;
    
    int main()
    {
      std::promise<int> p1, p2;
      std::future<int> f1 = p1.get_future();
      std::future<int> f2 = p2.get_future();
      
      p1.set_value(42);
      assert(f1.get() == 42);
      
      std::thread t([&]() {
        std::this_thread::sleep_for(100ms);
        p2.set_value(43);
      });
      
      auto start_time = std::chrono::system_clock::now();
      assert(f2.get() == 43);
      std::chrono::duration<double, std::milli> elapsed = std::chrono::system_clock::now() - start_time;
      std::cout << "Waited " << elapsed.count() << " ms\n";
      t.join();
      return 0;
    }
    
    // output: Waited 100.253 ms                                                                                                                                                       
    

    一个future对象可以通过promise.get_future()方法创建出来。当我们有真正的值来填进promose对象的时候,就用promise.set_value(v)方法。同时,(一般是在另一个线程里)当准备要获取值的时候,就调用future.get()方法。get方法会保持阻塞状态直到一个有效值被填进去。

    值得一提的是,有一个特化的T = void。这货有啥用呢?可以用来阻塞等待某个并行任务完成:

    std::promise<void> ready_p;
    std::future<void> read_f = ready_p.get_future();
    
    std::thread thread_b([&]() {
        prep_work();
        ready_p.set_value(); // no arg
        main_work();
    });
    ready_f.wait();
    // now that thread B has completed
    

    A bit of details

    需要留意的是,promise也好future也罢,都是有动态内存分配(dynamic memory allocation)的开销的。
    std_promise_future_schema.PNG(图源为参考文献)

    留意图中的那个State对象,它基本上是一个shared_ptr——因为虫洞的两端(很可能是不同线程)都要用到这个共享对象(shared ownership)。所以创建std::promose/std::future的时候都是要申请新的堆空间。

    Reference

    本文全文参考自本书:
    <Mastering the C++17 STL: Make Full Use of the Standard Library Components in C++17>

  • C++17 std::variant 简单使用

    std::variant应当是用来替代union来使用的,后者从C时代就有了,但是它缺乏类型安全的保障。boost库很早之前就有了实现(其实从C++11开始,感觉boost不知道多少东西被借鉴走了)。
    std::variant的类长这个样子:

    template <class... Types>
    class variant;
    

    Usage

    使用起来也是比较方便的

    std::variant<int, double> v1;
    v1 = 1; // activate the "int" member
    assert(v1.index() == 0);
    assert(std::get<0>(v1) == 1);
    
    v1 = 3.14; // activate the "double" member
    assert(v1.index() == 1);
    assert(std::get<1>(v1) == 3.14);
    assert(std::get<double>(v1) == 3.14);
    
    assert(std::holds_alternative<int>(v1) == false);
    assert(std::holds_alternative<double>(v1) == true);
    
    assert(std::get_if<int>(&v1) == nullptr);
    assert(*std::get_if<double>(&v1) == 3.14);
    

    上面代码里的几个好用的constexpr函数:

    • std::get<>:可以传入index或是type。如果index或者类型不对的话,会跑出std::bad_variant_access异常;
    • std::get_if<>:这个是不抛异常的版本。注意参数和返回值类型都是指针——如果没有有效元素,那就会返回nullptr;
    • std::holds_alternative<Type>:判断一个Type在不在定义的variant参数类里面;

    Visiting variants

    如果要根据当前存储的类型来对variant对象做一些操作,第一会想到写一些if语句

    if (std::holds_alternative<int>(v1) == true) {
      // handle int value
    }
    else {
      // handle double value
    }
    

    但是如果variant的可选类型比较多的话,定义一个Visitor类会有助于按类型来执行操作,逻辑会显得清晰

    struct Visitor {
      double operator() (double d) { return d; }
      double operator() (int i) { return double(i); }
    };
    
    void show(std::variant<double, int> v) {
      std::cout >> std::visit(Visitor{}, v) << std::endl;
    }
    
    void test() {
      show(3.14);
      show(1);
    }
    
  • C++17 std::generate的使用

    std::generate是“遍历——执行”工具函数的一种

    #include <algorithm>
    #include <iostream>
    #include <string>
    #include <vector>
    
    int main()
    {
      std::vector<std::string> v(4);
      std::generate(v.begin(), v.end(), [i=0]() mutable {
          return ++i % 2 ? "hello" : "world";
      });
      
      for (auto&& s : v) {
        std::cout << s << " ";
      }
      return 0;
    }
    

    上面的函数输出是hello world hello world .

    std::generate(beginIt, endIt, generator)函数接受起止的迭代器以及一个生成方法。

  • C++17 新特性—— if constexpr

    看代码说话

    template <typename Iterator>
    auto distance(Iterator begin, Iterator end) {
      using Traits = std::iterator_traits<Iterator>;
      if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename Traits::iterator_category>) {
        return end - begin;
      } else {
        auto result = typename Traits::difference_type();
        for (auto it = begin; it != end; ++it) {
          ++result;
        }
        return result;
      }
    }

    上面的代码用来判断两个iterator之间的距离。主要逻辑是判断iterator是否是可随机访问的iterator然后作分支处理,可以随机访问的迭代器直接把iterator相减。
    constexpr if语句中,条件的值必须是可按语境转换到 bool 类型的经转换常量表达式。若其值为 true,则舍弃 false分支语句(若存在),否则舍弃 true分支语句。有意思的一点是,被舍弃语句中的 return 语句不参与函数返回类型推导,也就是说两个分支语句中的return推倒出来的类型允许不一样。

    如果没有C++17这种怎么写呢?就是比较麻烦。
    一可以加一个参数,但是每个tag写一个

    template <typename Iterator>
    auto distance(Iterator begin, Iterator end, std::bidirectional_iterator_tag); // one for each tag type

    或者用SFINAE写两个

    template <typename Iterator>
    auto distance(Iterator begin, Iterator end, 
                  typename std::enable_if_t<std::is_base_of_v<一大坨>>* = nullptr);
  • std::unique_ptr 关于智能指针的种种

    这个域名也是突发奇想买到的,作为unique-ptr.com,为了对得起这个名儿,那就说一下智能指针算了。

    前传。裸指针和auto_ptr

    裸指针就是高度自治(要自己管)的指针,在符合基本法(误)的前提下,使用裸指针的人可以为所欲为。裸指针的一个问题是,它有点不符合现代C++内存管理的指导思想,使用者必须对它的生命周期负责,否则有可能发生:

    • 内存泄漏(memory leak):如果new一大片内存但是由于代码复杂到头来忘了释放,就有可能耗尽内存资源;内存泄漏还有一种情况:如果申请内存做事情做了一半抛出了异常——代码跑到了exception的花括号里之后,原来申请的内存直接没办法拿到并且释放;
    • 悬挂指针(dangling reference):指针已经被删除了,但其内存仍然在使用中;另外还有双重删除(double delete)的问题,当程序的某部分要删除一个已经被删除的指针时,即可出现这种情况;还有一种,指针所指的内存已经失效了(比如出了作用域了),但是后面的代码还用着指针;

    应该是为了缓解这类问题,C++ 98标准搞出来了个智能指针:auto_ptr。相当于加了一个“管家”,在对象的内部保存那个裸指针,在管家析构的时候自动释放裸指针的内容,如果赋值的话那就把裸指针的所有权转交给新的管家。
    但是由于移动(move)语义是C++11才有的,auto_ptr在实现的时候,如果在两个智能指针对象之间作赋值(operator=)或者拷贝构造,它的参数都是非const类型的。为啥呢?因为它是会调用original_auto_ptr.release()方法拿到原始裸指针。
    这个有点无奈的操作是为了实现指针所指对象的“占有权”管理:两个auto_ptr不允许管理同一个目标裸指针。在“拷贝”的时候,相当于做了“主权移交”。

    Copying an auto_ptr copies the pointer and transfers ownership to the
    destination: both copy construction and copy assignment of auto_ptr
    modify their right hand arguments, and the “copy” is not equal to the
    original.
    https://en.cppreference.com/w/cpp/memory/auto_ptr

    这种操作并不符合常人对“拷贝“的理解,更像是”剪切“操作。且正是因为这个问题,切不可把auto_ptr放到stl容器里面,否则会得到各种编译错误。典型案例是这个:https://stackoverflow.com/questions/111478/why-is-it-wrong-to-use-stdauto-ptr-with-standard-containers
    另外由于析构的时候是用delete,auto_ptr里也不能放指针数组(那种需要用delete[]).

    unique_ptr

    不同于auto_ptr,unique_ptr 的赋值运算符只接受典型地由 std::move 生成的右值。刚才说了,“右值”是C++11移动语义的产物,它比较好地解决了主权移交时的尴尬问题。这怎么说呢,一个智能指针的命运啊,当然要靠自我奋斗,但是也要考虑到历史的行程
    回顾看之前说的裸指针的两个问题:

    • 内存泄漏:由于析构函数里会delete管理的裸指针对象,所以在跑出对象作用域的时候,编译器会帮我们解决好问题,不用操心了;
    • 悬挂指针:缓解,但不能完全避免。

    一个例子是

    #include <iostream>
    #include <memory>
    #include <string>
        
    int main()
    {
      std::unique_ptr<std::string> str_ptr;
      {
          std::string tmp("bbbababa");
          str_ptr.reset(&tmp);
          std::cout << str_ptr.get() << std::endl;
          std::cout << *str_ptr << std::endl;
      }
      std::string tmp("xxxxxxx");
      std::cout << str_ptr.get() << std::endl;
      std::cout << *str_ptr << std::endl;
      return 0;
    }
    // Output:
    //   0x728562b4f060
    //   bbbababa
    //   0x728562b4f060
    //   xxxxxxx
    

    此外,数组的问题也通过特化std::unique_ptr<T[]> 得到了解决.
    可能有些同学会有性能方面的顾虑,觉得智能指针会比裸指针的操作性能差一点。坦白讲这个是有可能的,但是在实际使用的过程中,往往作profiling后会发现性能出现瓶颈的地方几乎不是智能指针。作为unique_ptr来讲,它不需要维护额外的状态,只是对象的创建和销毁要作一些特殊处理,平时使用的过程中完全可以当作裸指针来用。真正可能要担心性能问题的,是shared_ptr这一类允许共享所有权的智能指针。

  • 记一次gtest EXPECT_DEATH遭遇sigsegv的debug过程

    the crash

    实习生小哥上周突然发现单元测试在dev机器上跑不起来了,–verbose一看发现是gtest报错Failure death test:
    gtest_expect_death_failure.png

    现象其实是所有写了EXPECT_DEATH的单元测试都挂掉了,感觉问题并不简单~ 问了一下边上的几位最近代码有什么变动,一说是用了个新的计算库。

    gdb

    gdb一波,除看代码发现这个测试的原理是起一个子进程,然后把stderr写到一个临时文件里,子进程退出后,从临时文件里读取log信息,和预期值比对。发现第一现象是stderr没抓到,结果在找stderr为什么没抓到的邪路上走了很久,发现根本没产生stderr:/tmp下面写的文件,大小是0字节。

    然后继续gdb,由于是要跟踪子进程,要把选项打开:

    set detach-on-fork off
    set follow-fork-mode child

    run了一下,看到了一点端倪:其实在fork之后进程就sigsegv了,而且居然是在动态加载glibc里的chdir函数过程中挂的。查了一下,机器上的glibc已经至少一年没有动过了,因此基本排除是glibc自己有问题。

    然后开始各种翻来覆去的看,先看了几个local变量,发现一切都显得那么完美,根本没有任何内存值被搞烂的迹象。接着只能看挂掉的现场了,info registers看到寄存器的值如下:
    gdb_info_registers.png

    突然发现红框处的rsp地址有点太“整”了,google了一下,确认这个地址应该是个用户态栈地址的下界。然后查看一下当前的指令,发现是一条push %ebx,也就是说还准备压栈————挂掉的清晰一些了,其实是栈地址空间不够了,也就是传说中的stack overflow。这也是为什么gtest能够看到程序挂掉,但是拿不到预期的错误信息的原因。

    the stack overflow

    那么,为什么栈空间不够了呢?翻了一下frame 0里的$rsp值,发现在0x7fc0附近,和刚才看到顶上的$rsp=0x7000相差了4032,也就是估计这个程序栈总的大小在4kb。查了一下ulimit -s有32767,诶? 于是接着看代码,发现gtest-death-test.cc里的做法, ExecDeathTestSpawnChild()里写道

    • mmap一个stack,它的大小是getpagesize() // ==4KB
    • clone一个新的线程来跑测试例,用的这个新创建出来的stack

    真相大白了。解决方法是mmap的时候分配多一点的栈空间。至于为什么到了上周才触发,大概是因为.so又多了几个吧~

    后记

    当然,其实这个bug早就有fix了,只是我是后来通过搜”ExecDeathTestChildMain stack overflow”这个关键词才找到的。之前搜了半天EXPECT_DEATH没什么结果~
    相关的PR有:

    所以说thirdparty code还是要偶尔更新一下的呀~