标签: c++11

  • C++ SFINAE 检测是否存在某个名称的成员函数

    SFINAE(Substitution failure is not an error) 被用在很多模板的花式操作里,笔者使用的时候多半是为了将运行时的多态替换成编译器的静态多态。

    简单版:检测一个类T是否存在某名字的成员函数

    举个例子,有如下的类定义:

    class SomeWhatClass {
    public:
      void SomeWhatFunction(int x, double y);
    };

    希望能有一个静态的检测,能够实现

    static_assert(HasSomeWhatFunction<SomeWhatClass>::value, "expecting true");

    那么可以这样[1]:

    template <typename T>
     class HasSomeWhatFunction {
       typedef char one;
       struct two { char x[2]; };
       template <typename C>
       static one test(decltype(&C::SomeWhatFunction));
       template <typename C>
       static two test(…);
     public:
       enum { value = sizeof(test(0)) == sizeof(char) };
     };

    基本逻辑是如果被测试的typename T有成员函数,那么调用HasSomeWhatFunction的静态方法test<T>()时,模板替换是按照返回值类型为one(char)来生效的;反之如果不存在SomeWhatFunction的函数,那么返回类型one的模板替换失败,继续查找,匹配到万能重载(catch-all overload) 的static two test(…)里。

    这种做法的可读性在模板的骚操作里算是好的,但是它无法区分SomeWhatFunction的函数签名(参数类型、返回值类型)。

    普通版:检测一个类T是否存在某签名的成员函数

    接上个例子拓展一下:

    class SomeWhatClass {
    public:
      void SomeWhatFunction(int x, double y);
    };
    
    // 希望能检测函数签名
    static_assert(!HasSomeWhatFunction_Double<SomeWhatClass>::value, "not expecting");
    static_assert(HasSomeWhatFunction_Int_Double<SomeWhatClass>::value, "expecting");

    这个就需要更复杂一些的逻辑了:

     template <typename T>
     class HasSomeWhatFunction_Double {
       typedef char one;
       struct two { char x[2]; };
       
       template <typename C, void (C::*)(double)>
       struct Check;
       
       template <typename C>
       static one test(Check<C, &C::SomeWhatFunction> *);
       
       template <typename C>
       static two test(…);
     public:
       enum { value = sizeof(test(0)) == sizeof(char) };
     };
    
     template <typename T>
     class HasSomeWhatFunction_Int_Double {
       typedef char one;
       struct two { char x[2]; };
       
       template <typename C, void (C::*)(int, double)>
       struct Check;
       
       template <typename C>
       static one test(Check<C, &C::SomeWhatFunction> *);
       
       template <typename C>
       static two test(…);
     public:
       enum { value = sizeof(test(0)) == sizeof(char) };
     };

    可以看到这个实现比之前的多定义了一个内部类Check。在初始化enum value值时会调用test,然后会尝试模板替换,进而想要去特化struct Check。如果失败了(不存在对应签名的函数),那么会回滚到static two test里。

    这个实现其实能够覆盖许多场景了,但是还有一个奇怪的需求满足不了:如果要判断一个类的模板成员函数呢?

    地狱版:检测一个类T是否存在某签名的模板成员函数

    小改一下SomeWhatClass

    class SomeWhatClass {
    public:
      template <typename T>
      void SomeTempMemberFunc(int X, T& t);
    };

    如果需要检测void SomeTempMemberFunc<int, T&>是否存在,咋办? 搜了一圈,解释的最清楚的是参考[2]里面描述的,利用std::declval的解法。

    template <typename T>
    class HasTempMemberFunc {
      struct dummy {};
      constexpr static int int_holder {};
      
      template <typename C, typename P>
      static auto test(P& p) 
      -> decltype(std::declval<C>().SomeTempMemberFunc(int_holder, p), std::true_type());
    
      template <typename C, typename P>
      static std::false_type test(...);
    public:
      static const bool value = std::is_same<std::true_type, 
                                             decltype(test<T, dummy>(nullptr))>::value;
    };

    几个知识点:

    • decltype里利用了含有逗号运算符[3]的表达式;
    • declval可以避开构造函数而使用类成员;

    参考

    [1] writing and using a C++ template to check for a function’s existence http://www.cplusplus.com/forum/beginner/70134/

    [2] SFINAE Hell: detecting template methods https://blog.quasardb.net/2015/04/12/sfinae-hell-detecting-template-methods

    [3] SFINAE示例 https://zh.cppreference.com/w/cpp/language/sfinae#.E7.A4.BA.E4.BE.8B

  • 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>

  • 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这一类允许共享所有权的智能指针。