如何延迟初始化 unique_ptrs 的模板元组?

How to lazily initialize a template tuple of unique_ptrs?

我需要编写一个 class 来创建基于异步回调的数据快照。例如,如果我有一个整数流和一个双流,我必须将数据推入队列并在它们都有值时处理数据。我的 class 看起来像这样的 atm:

template<typename... Types>
class Dataholder{
private:
 std::deque<std::tuple<std::unique_ptr<Types>...>> data_frame_deque_;
public:
  template <class T>
  void push(T data); //find the next tuple that doesn't yet have value for type T and add it, or create new tuple
  void run_if_frame_ready(std::function<void (std::tuple<std::unique_ptr<Types>...>)&> fun); //run function fun, that accepts a tuple as defined in data_frame_queue_;
}

示例用法应如下所示:

Dataholder<int, double> holder;
void myProcessingFunction(std::tuple<int,double>& data)
{
 //do stuff
}
int main(void)
{
  holder.push(42);
  holder.push(3.14);
  holder.run_if_frame_ready(myProcessingFunction);
}

Dataholder::push()函数检查dequeue中的每个tuple,是否存在frame,如果存在,是否初始化对应的值。如果不是,则初始化它,如果是,则移动到下一个dataframe/create一个新的空帧并将数据放在那里。

Dataholder::run_if_frame_ready() 函数检查队列中的第一帧是否已完全初始化。如果是,则将其弹出并将其传递给给定函数。

我的问题是,如何使用 C++ 模板正确初始化所有成员?如何实现推送功能?我知道我可以用 std::is_same 之类的东西进行检查,但我不太明白如何做。暂时所有的类型都只用一次还好,但是同类型的多个entry可以实现吗?例如。那么对于 class 像 Dataholder<int, int>?

这样使用的情况,推送函数可以是 push(T data, int index)

您的 DataHolder 可以这样实现:

template<typename... Types>
class Dataholder {
public:
  // alias for entry_type
  using entry_type = std::tuple<std::unique_ptr<Types>...>;

  template <class T>
  void push(T&& data); // implemented in the .h file, but outside the class

  size_t size() const {
    return data_frame_deque_.size();
  }

  bool frame_ready() const {
    // C++17 fold expression
    // (link for a C++14 version without fold expression provided below)
    return (std::get<std::unique_ptr<Types>>(data_frame_deque_.front()) && ...);
  }

  void run_if_frame_ready(std::function<void(entry_type&&)>&& fun) {
    if(frame_ready()) {
      fun(std::move(data_frame_deque_.front()));
      data_frame_deque_.pop_front();
    }
  }

private:
  std::deque<entry_type> data_frame_deque_;
};

带推送功能:

template<typename... Types>
template <class T>
void Dataholder<Types...>::push(T&& data) {

  // find the next tuple that doesn't yet have value for type T
  std::unique_ptr<T>* placeholder = nullptr;
  for(auto& tup : data_frame_deque_) {
    auto& uptr = std::get<std::unique_ptr<T>>(tup);
      if (!uptr) {
        placeholder = &uptr;
        break;
      }
  }

  // if all tuples already have this type, create a new one
  if (placeholder == nullptr) {
    data_frame_deque_.push_back(entry_type());
    placeholder = &std::get<std::unique_ptr<T>>(data_frame_deque_.back());
  }

  // add the item into the tuple
  *placeholder = std::make_unique<T>(std::forward<T>(data));
}

Code above (C++17)

Code for C++14, without fold expression

请注意,如果可以从不同线程使用代码,则应添加适当的锁定以保护对双端队列的访问。

我对做这么简单的事情所涉及的困难感到非常惊讶。这是你有双端队列元组的实现,它最终和你手工做的一样高效,即你不会在队列中循环寻找空条目。

template<typename>
void get();  // for ADL

template<typename... Types>
class Dataholder{
    using Deques = std::tuple<std::deque<std::unique_ptr<Types>>...>;

    Deques deq;

    template<typename F, size_t... Is>
    void run(F&& f, std::index_sequence<Is...>)
    {
        std::forward<F>(f)(*(get<Is>(deq).front())...);
    }

    template<size_t... Is>
    bool empty(std::index_sequence<Is...>) const
    {
        // mock fold expression
        bool e = false;
        char unused[] = {(e |= get<Is>(deq).empty())...};
        (void)unused;
        return e;
    }

    template<size_t... Is>
    void pop(std::index_sequence<Is...>)
    {
        char unused[] = {(get<Is>(deq).pop_front(), 0)...};
        (void)unused;
    }

public:
    // push by index
    template<size_t I, typename T>
    void push(T&& data)
    {
        using Tuple = std::tuple<Types...>;
        using type = std::tuple_element_t<I, Tuple>;
        get<I>(deq).emplace_back(new type(std::forward<T>(data)));
    }

    // push by type
    template<typename T>
    void push(T&& data)
    {
        using type = std::deque<std::unique_ptr<std::remove_reference_t<T>>>;
        get<type>(deq).emplace_back(new auto(std::forward<T>(data)));
    }

    bool empty()
    {
        return empty(std::index_sequence_for<Types...>{});
    }

    void pop()
    {
        pop(std::index_sequence_for<Types...>{});
    }

    // you don't want std::function for this
    template<typename F>
    void run_if_frame_ready(F&& f)
    {
        if(empty())
            return;
        run(std::forward<F>(f), std::index_sequence_for<Types...>{});
    }
};

和运行作为

void f(int i, double d)
{
    std::cout << i << ' ' << d;
}

int main()
{
    Dataholder<int, double> holder;
    holder.push(42);
    holder.run_if_frame_ready(f);  // does nothing
    holder.push(3.14);
    holder.run_if_frame_ready(f);
}

最后我选择了 Passer Bys 版本,因为他们的解决方案允许将 class 与可能出现的多个相同类型的实例一起使用。还弄清楚了如何将框架弹出到一个变量中并在以后使用它以便可以引入锁定。还将一些 public 接口移至私有,因为我的用例不需要它们。如果您将其用于其他用途并需要访问某些私有功能,请确保所有内容都已正确锁定。这是最终产品:

#include <deque>
#include <tuple>
#include <memory>
#include <utility>
#include <mutex>

template<typename>
void get();  // for ADL

template<typename... Types>
class Dataholder{
private:
    using Deques = std::tuple<std::deque<std::unique_ptr<Types>>...>;

    Deques deq;
    std::mutex mutex_;

    template<typename F, size_t... Is>
    void run(F&& f, std::index_sequence<Is...>)
    {   
        mutex_.lock();
        if(!empty())
        {
            auto t = std::make_tuple(*(get<Is>(deq).front())...);
            pop();
            mutex_.unlock(); //unlock mutex before processing the removed data
            std::forward<F>(f)(std::get<Is>(std::move(t))...);
        }
        else
        {
            mutex_.unlock();
        }
    }

    template<size_t... Is>
    bool empty(std::index_sequence<Is...>) const
    {
        //make sure this is locked by convention!
        // mock fold expression
        bool e = false;
        char unused[] = {(e |= get<Is>(deq).empty())...};
        (void)unused;
        return e;
    }

    template<size_t... Is>
    void pop(std::index_sequence<Is...>)
    {
        //disable type narrowing warning for unused variable
        #pragma GCC diagnostic push
        #pragma GCC diagnostic ignored "-Wnarrowing"
        //we need this for being able to resolve the pop_front call
        char unused[] = {(get<Is>(deq).pop_front(), 0)...};
        (void)unused;
        #pragma GCC diagnostic pop
    }

    bool empty()
    {
        return empty(std::index_sequence_for<Types...>{});
    }

    void pop()
    {
        pop(std::index_sequence_for<Types...>{});
    }

public:
    // push by index
    template<size_t I, typename T>
    void push(T&& data)
    {
        using Tuple = std::tuple<Types...>;
        using type = std::tuple_element_t<I, Tuple>;
        std::lock_guard<std::mutex> guard(mutex_);
        get<I>(deq).emplace_back(new type(std::forward<T>(data)));
    }

    // push by type
    template<typename T>
    void push(T&& data)
    {
        using type = std::deque<std::unique_ptr<std::remove_reference_t<T>>>;
        std::lock_guard<std::mutex> guard(mutex_);
        get<type>(deq).emplace_back(new auto(std::forward<T>(data)));
    }

    template<typename F>
    void run_if_frame_ready(F&& f)
    {
        //locked in private callee
        run(std::forward<F>(f), std::index_sequence_for<Types...>{});
    }
};