C++ 中文周刊 第83期

reddit/hackernews/lobsters/meetingcpp/purecpp知乎/等等摘抄一些c++动态

周刊项目地址在线地址知乎专栏 腾讯云+社区

欢迎投稿,推荐或自荐文章/软件/资源等

可以贴在下一期草稿里 草稿链接

2022 1008


资讯

标准委员会动态/ide/编译器信息放在这里

编译器信息最新动态推荐关注hellogcc公众号 本周更新 2022-10-05 第170期

文章

直接贴个代码 https://godbolt.org/z/Yxc58vrWW

#include <cassert>
#include <coroutine>
#include <sstream>
#include <string_view>
#include <optional>

struct task {
  struct promise_type {
    task get_return_object() { return {}; }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    template<class T> void return_value(T) { }
    void unhandled_exception() {}
  };
};

template<class T>
class awaiter {
 public:
  auto operator co_await() {
    struct {
      awaiter& aw;

      auto await_ready() const noexcept -> bool { return static_cast<bool>(aw.value); }
      auto await_suspend(std::coroutine_handle<> coroutine) noexcept {
        aw.coroutine = coroutine;
        return true;
      }

      auto await_resume() const noexcept {
        struct reset {
         std::optional<T>& value;
          ~reset() { value = {}; }
        } _{aw.value};
        return *aw.value - '0';
      }
    } awaiter{*this};

    return awaiter;
  }

  void process(const T& value) {
    this->value = value;
    coroutine.resume();
  }

 private:
  std::optional<T> value{};
  std::coroutine_handle<> coroutine{};
};

class parser {
 public:
  explicit parser(std::stringstream& out) : out{out} { }
    
  void parse(std::string_view in) {
    for (const auto value : in) {
      input.process(value);
    }
  }
    
 private:    
  task parse_impl() {
    enum state { A, B, C } state;
    for (;;) {
      const auto value = co_await input;
      const auto in = value != 0;
      switch (state) {
          case A:
              out << '0';
              state = in ? B : A;
              break;
          case B:
              out << '0';
              state = in ? C : A;
              break;
          case C:
              out << (in ? '0' : '1');
              state = in ? C : A;
              break;
      };
    }
  }

  awaiter<char> input{};  
  task start{parse_impl()};
  std::stringstream& out;
};

int main() {
  {
    std::stringstream out{};
    parser p{out};
    p.parse("0");
    assert("0" == out.str());
  }

  {
    std::stringstream out{};
    parser p{out};
    p.parse("01");
    assert("00" == out.str());
  }
    
  {
    std::stringstream out{};
    parser p{out};
    p.parse("0110");
    assert("0001" == out.str());
  }    

  {
    std::stringstream out{};
    parser p{out};
    p.parse("0001");
    assert("0000" == out.str());
  }

  {
    std::stringstream out{};
    parser p{out};
    p.parse("000110");
    assert("000001" == out.str());
  }

  {
    std::stringstream out{};
    parser p{out};
    p.parse("0110100010010001101001000111110010011001");
    assert("0001000000000000010000000000001000000100" == out.str());
  }
}

cppfront的设计。看个乐。草木大哥上次玩的constraint也探索了好几年。这个玩意我估计也得玩几年。不会进。只是提供个思路

众所周知,range for里的临时变量左值有问题

class Keeper {  
  std::vector<int> data{2, 3, 4};
public:
  ~Keeper() { std::cout << "dtor\n"; }
  // Returns by reference
  auto& items() { return data; }
};
// Returns by value
Keeper GetKeeper() {
  return {};
}
void Use() {
  // ① Use the result of GetKeeper and return
  // over items
  for(auto& item : GetKeeper().items()) {
    std::cout << item << '\n';
  }
}

这个遍历很有可能挂掉。UB。但是我非要这么写,keeper类怎么设计呢?

class Keeper {
  std::vector<int> data{2, 3, 4};
public:
  ~Keeper() { std::cout << "dtor\n"; }
  auto& items() & { return data; }
  // ④ For rvalues, by value with move
  auto items() && { return std::move(data); }
};

注意这两个items后面的&限定,两种限定约定了被调用的时候走左还是右值,如果是range for循环,就调用第二个items,救一下data的生命,就没问题了。

我觉得还是尽量别range for里乱搞。容易误用。有的类设计类这种方法,如果有的类没这么设计,不就完了。

周所周知,临时变量的生命周期是一行,来一个复杂的例子

考虑一行上锁自动解锁

template<typename> struct LockableData;

namespace std
{
    template<typename Data>
    struct default_delete<LockableData<Data>>
    {
        void operator()(LockableData<Data>* p)
        const noexcept { p->m.unlock(); }
    };
}

template<typename Lockable>
struct [[nodiscard]] LockedData
{
    LockedData(Lockable* l = nullptr) : l(l)
    { if (l) l->m.lock(); }

    auto operator->() const noexcept
    { return std::addressof(l->data); }

private:
    std::unique_ptr<Lockable> l;
};

template<typename Data>
struct LockableData
{
    LockedData<LockableData> Lock() { return this; }

private:
    friend struct LockedData<LockableData>;
    friend struct std::default_delete<LockableData>;

    std::mutex m;
    Data data;
};

使用例子

struct WidgetInfo
{
    std::string name;
    int times_toggled = 0;
};

class Widget
{
    LockableData<WidgetInfo> info;

public:
    void SetName(std::string name)
    {
        auto lock = info.Lock();
        lock->name = name;
        lock->times_toggled = 0;
    }

    std::string GetName()
    {
        auto lock = info.Lock();
        return lock->name;
    }

    void Toggle()
    {
        { // scope the lock
            auto lock = info.Lock();
            lock->times_toggled++;
        }
        FlipSwitchesRandomly();
    }
};

目前来看还是没啥问题,但是要多一个lock,很自然的,你想到了省略这一行

template<typename Data>
struct LockableData
{
    LockedData<LockableData> Lock() { return this; }
    auto operator->() { return Lock(); } // NEW!

private:
    friend struct LockedData<LockableData>;
    friend struct std::default_delete<LockableData>;

    std::mutex m;
    Data data;
};

class Widget
{
    LockableData<WidgetInfo> info;

public:
    void SetName(std::string name)
    {
        auto lock = info.Lock();
        lock->name = name;
        lock->times_toggled = 0;
    }

    std::string GetName()
    {
        return info->name; // lock-read-unlock
    }

    void Toggle()
    {
        info->times_toggled++; // lock-modify-unlock
        FlipSwitchesRandomly();
    }
};

问题来了。info->调用生成了一个临时对象,临时对象这一行结束就释放了,可能会出现读的不一样的问题,但这问题不大,真正的问题是这种用法可能导致锁两次

比如上面这个toggle,伪代码

    // Evaluate right hand side
    LockedData<WidgetInfo> lock1 = info.operator->();
    int rhs = std::max(lock1->times_toggled, 10);

    // Evaluate left hand side
    LockedData<WidgetInfo> lock2 = info.operator->();

    // Perform the assignment
    lock2->times_toggled = rhs;

    // Destruct temporaries in reverse order of construction
    destruct lock2;
    destruct rhs;
    destruct lock1;

明显锁了两次。可能->这个方法过于有问题,我直接调用,比如

    std::string GetName()
    {
        return info.Lock()->name;
    }

应该不会有问题了吧, 如果toggle这么实现

    void Toggle()
    {
        // suspicious double-lock - more likely to be spotted in code review
        info.Lock()->times_toggled = std::max(info.Lock()->times_toggled, 10);
        FlipSwitchesRandomly();
    }

也是有同样问题的

RAII的烦恼也很多啊。解决方法可能是out_ptr或者std::synchronized_value folly::synchronize这种类似的玩意。别自己写了。可能想不到

不太懂

分析了一波,是编译器bug。msvc 16.10 以下的版本有问题,修复记录 https://devblogs.microsoft.com/cppblog/cpp20-coroutine-improvements-in-visual-studio-2019-version-16-11/

用concept实现crtp。之前也介绍过类似的

// we create a concept can_work to check if do_work is implemented
// this will describe our interface
template <typename T>
concept can_work = requires(T t) {
    t.do_work();
};

// now we apply this concept to an empty type which represents a worker (or our base class)
template<can_work T>
struct worker : public T {};

// now create a concrete worker (corresponding derived) where we implement do_work
struct concrete_worker {
    void do_work() {
        // ...
    }
};

// nice to have: an alias for our concrete worker
using my_worker = worker<concrete_worker>;

//...
// which we can use now
my_worker w;
w.do_work();

面向接口的感觉

没啥说的。c++23就能用了。之前你可以用absl的或者boost的。都差不多

一个COW vector大概的样子

template <class T>
class CowVector {
    struct State {
        std::atomic<int> ref;
        size_t size;
        size_t capacity;

        T elements[];
    }
    State* state;

    // if we're not unique, we need to allocate
    // a new State and copy the elements.
    // if we are unique, this is a no-op.
    void copy_on_write();

public:
    // copy constructor *never* allocates.
    // just increments ref-count
    CowVector(CowVector const& rhs)
        : state(rhs.state)
    {
        ++state->ref;
    }

    // and the mutable and const accessors do different things
    auto operator[](size_t idx) -> T& {
        copy_on_write();
        return state->elements[idx];
    }

    auto operator[](size_t idx) const -> T const& {
        return state->elements[idx];
    }
};

怎么更干净更灵活的copy_on_write? 这套代码怎么用 Deducing this 改写

template <class T>
class CowVector {
public:
    auto operator[](this CowVector& self, size_t idx) -> T&;
    auto operator[](this CowVector const& self, size_t idx) -> T const&;
};

Self应该模版化

template <class T>
class CowVector {
    struct State { ... };
    State* state;

    // this one (potentially) copies
    auto get_state() -> State*;

    // this one doesn't, because const
    auto get_state() const -> State const* { return state; }
public:
    template <class Self>
    auto operator[](this Self& self, size_t idx)
        -> std::copy_const_t<Self, T>&
    {
        return self.get_state()->elements[idx];
    }
};


里面还讨论了很多边角场景,感兴趣的可以看看

一个map存数据,如果存在就不插入

object* retrieve_or_create(int id)
{
  static std::unordered_map<int, std::unique_ptr<object>> m;

  // see if the object is already in the map
  auto [it,b] = m.emplace(id, nullptr);
  // create it otherwise
  if(b) it->second = std::make_unique<object>(id); 
  return it->second.get();
}

很常规。问题在于object可能非常大,可能构造异常。try catch一下,正好有try_emplace这个接口

object* retrieve_or_create(int id)
{
  static std::unordered_map<int, std::unique_ptr<object>> m;

  auto [it,b] = m.try_emplace(id, std::make_unique<object>(id));
  return it->second.get();
}

但是问题并没有解决,我们希望的是,直到需要调用make的时候,再调用。推迟到emplace 那一刻

template<typename F>
struct deferred_call
{
  using result_type=decltype(std::declval<const F>()());
  operator result_type() const { return f(); }

  F f;
};

object* retrieve_or_create(int id)
{
  static std::unordered_map<int, std::unique_ptr<object>> m;

  auto [it,b] = m.try_emplace(
    id,
    deferred_call([&]{ return std::make_unique<object>(id); }));
  return it->second.get();
}

针对string char*转换问题,加个补丁

template<typename F>
struct deferred_call
{
  using result_type=decltype(std::declval<const F>()());
  operator result_type() const { return f(); }

  // "silent" conversion operator marked with ~explicit
  // (not actual C++)
  template<typename T>
  requires (std::is_constructible_v<T, result_type>)
  ~explicit constexpr operator T() const { return {f()}; }
  
  F f;
};

视频

decltype(auto)可以拿到真正的类型 auto有时候拿不到引用类型

开源项目需要人手

新项目介绍/版本更新

划水严重,保佑不被开。


本文永久链接

看到这里或许你有建议或者疑问或者指出错误,请留言评论! 多谢! 你的评论非常重要!也可以帮忙点赞收藏转发!多谢支持! 觉得写的不错那就给点吧, 在线乞讨 微信转账