首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >存储大量依赖于运行时的算术类型的数据结构。

存储大量依赖于运行时的算术类型的数据结构。
EN

Code Review用户
提问于 2016-06-23 17:20:21
回答 2查看 596关注 0票数 4

警告:很多代码。如果太多,请主要关注channel.hchannel.hpp

这是我第一次在代码评论上发帖,所以如果有什么不寻常的地方或者写得更好,我会提前道歉。

上下文

我使用这个数据结构的程序是一个DAQ到CSV转换工具。简单地说,目标是从二进制读取和写入文本。忽略一个描述其内容的小标题,DAQ文件是使用“框架”和“通道”构造的。每个帧都包含若干个通道。每个通道都有一个固定数量的项目。例如,我可以有三个通道: A、B和C,其中A包含每帧3项,B包含每帧1项,C包含每帧20项。

有三件事要考虑:首先,每个帧中的信道不能保证是连续的。使用上面的例子,在框架1上,我可能有通道A、B和C的数据,但是在框架2中,我可能只有A和C频道的数据。

第二,每个通道都有自己的算术类型,在头中用一个字符表示。例如,通道A可能包含'd‘(双)、B类型'i’(整数)和C类型'c‘(char)类型的数据。这意味着,即使是要读取的项的类型也要到运行时才知道。

第三,必须在程序中读取和存储所有这些数据,以便用户有选择地选择要打印的通道/数据,以及使用现有的通道创建自己的通道。这就要求存储方法在空间和速度上都是非常有效的。典型的DAQ文件范围从300 in到400 in,其中包含+65,000帧,+250个通道,以及一个通道中的+600项。作为透视,在一种情况下,我将DAQ文件中的所有项写入CSV。Excel报告了超过250万个细胞。

这是我为这个任务设计的数据结构(class Channel)。我已经包含了相关的文件和依赖项。我还提供了一个示例main.cpp来说明如何使用channel

代码使用模板来处理变量算术类型、智能指针和多态结构来存储模板结构,内部类型(Data::Type)与静态断言和类型安全异常相结合,以及C++14返回类型推断和一般lambda来访问数据。请告诉我你的想法。非常感谢!

main.cpp

代码语言:javascript
复制
#include "channel.h"

#include <iostream>
#include <vector>

int main() {
  // User-defined channels.
  auto c1 = Channel{Channel::create<int >("ints  :")};
  auto c2 = Channel{Channel::create<char>("chars :")};

  // Sample data.
  auto v1 = std::vector<int>{1, 2, 3};
  auto v2 = std::vector<char>{'a', 'b', 'c'};

  // Push-back method for user-defined channels.
  // User-defined channels can only have one item per frame.
  // SFINAE also provides clear error messages if a user
  // attempts to push back a type which does not match internals.
  for (auto i = std::size_t{}; i < v1.size(); ++i) c1.push_back(i, v1[i]);
  for (auto i = std::size_t{}; i < v2.size(); ++i) c2.push_back(i, v2[i]);

  // Push-back method for DAQ channels with multiple items.
  // During conversion, size of vector can range from 1 to +600.
  c1.push_back(4, v1);
  c2.push_back(4, v2);

  auto const print = [](auto const& data) {
    for (auto datum : data) std::cout << datum << ' ';
    std::cout << std::endl;
  };

  // Access data using generic lambdas.
  // This provides a type-safe way to access the templated
  // vector (or individual datum by frame and index) without
  // the user needing to know the underlying type (use casts).
  c1.data(print);
  c2.data(print);

  // Access data by frame and index (for channels with multiple items).
  // Channels should have a consistent number of items per frame, and
  // no duplicate frame numbers (preferably, in increasing order as well).
  c1.datum(2, 0, [](auto datum) { std::cout << datum << " == 3\n"; });
  c2.datum(4, 2, [](auto datum) { std::cout << datum << " == c\n"; });
}

记录.h

代码语言:javascript
复制
#ifndef RECORD_H
#define RECORD_H

#include "data.h"

#include <string>

struct Record {
  int32_t     id;                           // ID in DAQ file.
  int32_t     items;                        // Number of items.
  std::string name = std::string(53, '\0'); // Name of channel.
  int16_t     rate;                         // Rate of updates.
  Data::Type  type;                         // Type of data.
  int32_t     varlen;                       // Variable length (?)
};

#endif

data.h

代码语言:javascript
复制
#ifndef DATA_H
#define DATA_H

#include <stdexcept>
#include <vector>

// Abstract base class for DataT<T>.
struct Data {
  // Used to store internal type information in class Channel.
  enum struct Type : char {
    Double = 'd'
  , Float  = 'f'
  , Int    = 'i'
  , Short  = 's'
  , Char   = 'c'
  };

  // Template Metaprogramming. Returns corresponding Data::Type for type T.
  template <typename T> static Type get_type() {
    static_assert(std::is_arithmetic<T>::value, "type T must be arithmetic");
    if (std::is_same<T, double>::value) return Data::Type::Double;
    if (std::is_same<T, float >::value) return Data::Type::Float;
    if (std::is_same<T, int   >::value) return Data::Type::Int;
    if (std::is_same<T, short >::value) return Data::Type::Short;
    if (std::is_same<T, char  >::value) return Data::Type::Char;
    throw std::logic_error("invalid type T in Data::get_type<T>()");
  }

  // Mark class as abstract.
  virtual ~Data() = 0;
};

// Destructor must still be defined.
inline Data::~Data() = default;

// Concrete derived class used to store data in class Channel.
template <typename T> struct DataT : Data { std::vector<T> data; };

#endif

channel.h.h

代码语言:javascript
复制
#ifndef CHANNEL_H
#define CHANNEL_H

#include "data.h"
#include "record.h"

#include <memory>
#include <vector>

class Channel {
  std::unique_ptr<Data> m_data;
  std::vector<int>      m_frames;
  Record                m_record;

  // Private Constructor.
  Channel(std::unique_ptr<Data> data, Record record)
  : m_data(std::move(data)), m_record(std::move(record)) { }

  // Private Data Interface Function.
  template <typename Function> auto get_data(Function func);

public:
  // Disable default constructors. Enable moves only.
  Channel()                       = delete;
  Channel(Channel const&)         = delete;
  auto operator=(Channel const&)  = delete;
  Channel(Channel&&)              = default;

  // Factory Method Pattern.
  template <typename T> static Channel create(std::string name);

  static Channel create(Record record) {
    switch (record.type) {
    case Data::Type::Double : return {std::make_unique<DataT<double>>(), std::move(record)};
    case Data::Type::Float  : return {std::make_unique<DataT<float >>(), std::move(record)};
    case Data::Type::Int    : return {std::make_unique<DataT<int   >>(), std::move(record)};
    case Data::Type::Short  : return {std::make_unique<DataT<short >>(), std::move(record)};
    case Data::Type::Char   : return {std::make_unique<DataT<char  >>(), std::move(record)};
    default : throw std::invalid_argument("invalid Data::Type in Channel::create(Data::Type)");
    }
  }

  // Channel Interface Functions.
  decltype(m_frames) const& frames() const { return m_frames;        }
  decltype(m_record) const& record() const { return m_record;        }
  std::size_t               size()   const { return m_frames.size(); }

  // Data Interface Functions.
  template <typename T> decltype(DataT<T>::data) const& data() const;
  template <typename T> T const& datum(int frame, int offset) const;
  template <typename Function> auto data(Function func) const;
  template <typename Function> auto datum(int frame, int offset, Function func) const;

  // Record Interface Functions.
  decltype(Record::id)     const& id()     const { return m_record.id;     }
  decltype(Record::items)  const& items()  const { return m_record.items;  }
  decltype(Record::name)   const& name()   const { return m_record.name;   }
  decltype(Record::rate)   const& rate()   const { return m_record.rate;   }
  decltype(Record::type)   const& type()   const { return m_record.type;   }
  decltype(Record::varlen) const& varlen() const { return m_record.varlen; }

  // Channel Initialization Functions. Uses SFINAE for descriptive error messages.
  template <typename T> auto push_back(T)                                     -> std::enable_if_t<!std::is_arithmetic<T>::value>;
  template <typename T> auto push_back(int frame, T datum)                    -> std::enable_if_t< std::is_arithmetic<T>::value>;
  template <typename T> auto push_back(int frame, std::vector<T> const& data) -> std::enable_if_t< std::is_arithmetic<T>::value>;

private:
  // Enforce type match between internal type and template type deduction.
  template <typename T> void check_type() const;
};

#include "channel.hpp"

#endif

channel.hpp

代码语言:javascript
复制
/** Included by channel.h. **/

template <typename Function> auto Channel::get_data(Function func) {
  switch (m_record.type) {
  case Data::Type::Double : return func(static_cast<DataT<double>*>(m_data.get())->data);
  case Data::Type::Float  : return func(static_cast<DataT<float >*>(m_data.get())->data);
  case Data::Type::Int    : return func(static_cast<DataT<int   >*>(m_data.get())->data);
  case Data::Type::Short  : return func(static_cast<DataT<short >*>(m_data.get())->data);
  case Data::Type::Char   : return func(static_cast<DataT<char  >*>(m_data.get())->data);
  default : throw std::logic_error("invalid Data::Type set in Channel");
  }
}

template <typename T> Channel Channel::create(std::string name) {
  static_assert(std::is_arithmetic<T>::value, "type T must be arithmetic.");
  auto record = Record{0, 1, std::move(name), 1, Data::get_type<T>(), 0};
  return {std::make_unique<DataT<T>>(), std::move(record)};
}

template <typename T> decltype(DataT<T>::data) const& Channel::data() const try {
  check_type<T>();
  return static_cast<DataT<T> const*>(m_data.get())->data;
} catch (std::invalid_argument const& e) {
  throw std::invalid_argument(std::string(e.what()).append(" in Channel::data<T>()"));
}

template <typename T> T const& Channel::datum(int frame, int offset) const try {
  check_type<T>();
  auto index = std::distance(m_frames.begin(), std::lower_bound(m_frames.begin(), m_frames.end(), frame));
  return static_cast<DataT<T> const*>(m_data.get())->data[index * m_record.items + offset];
} catch (std::invalid_argument const& e) {
  throw std::invalid_argument(std::string(e.what()).append(" in Channel::datum<T>(int, int)"));
}

template <typename Function> auto Channel::data(Function func) const {
  switch (m_record.type) {
  case Data::Type::Double : return func(static_cast<DataT<double> const*>(m_data.get())->data);
  case Data::Type::Float  : return func(static_cast<DataT<float > const*>(m_data.get())->data);
  case Data::Type::Int    : return func(static_cast<DataT<int   > const*>(m_data.get())->data);
  case Data::Type::Short  : return func(static_cast<DataT<short > const*>(m_data.get())->data);
  case Data::Type::Char   : return func(static_cast<DataT<char  > const*>(m_data.get())->data);
  default : throw std::logic_error("invalid Data::Type set in Channel");
  }
}

template <typename Function> auto Channel::datum(int frame, int offset, Function func) const {
  auto index = std::distance(m_frames.begin(), std::lower_bound(m_frames.begin(), m_frames.end(), frame));
  switch (m_record.type) {
  case Data::Type::Double : return func(static_cast<DataT<double> const*>(m_data.get())->data[index * m_record.items + offset]);
  case Data::Type::Float  : return func(static_cast<DataT<float > const*>(m_data.get())->data[index * m_record.items + offset]);
  case Data::Type::Int    : return func(static_cast<DataT<int   > const*>(m_data.get())->data[index * m_record.items + offset]);
  case Data::Type::Short  : return func(static_cast<DataT<short > const*>(m_data.get())->data[index * m_record.items + offset]);
  case Data::Type::Char   : return func(static_cast<DataT<char  > const*>(m_data.get())->data[index * m_record.items + offset]);
  default : throw std::logic_error("invalid Data::Type set in Channel");
  }
}

// SFINAE Catch-All Function. Prints descriptive error message in case of type deduction failure.
template <typename T> auto Channel::push_back(T)
-> std::enable_if_t<!std::is_arithmetic<T>::value> {
  static_assert(std::is_arithmetic<T>::value, "type T must be arithmetic");
}

template <typename T> auto Channel::push_back(int frame, T datum)
-> std::enable_if_t<std::is_arithmetic<T>::value> try {
  this->check_type<T>();
  m_frames.emplace_back(frame);
  this->get_data([&datum = datum](auto& m_data) { m_data.push_back(std::move(datum)); });
} catch (std::invalid_argument const& e) {
  throw std::invalid_argument(std::string(e.what()).append(" in Channel::push_back<T>(int, T)"));
}

template <typename T> auto Channel::push_back(int frame, std::vector<T> const& data)
-> std::enable_if_t<std::is_arithmetic<T>::value> try {
  this->check_type<T>();
  m_frames.emplace_back(frame);
  this->get_data([&](auto& m_data) { m_data.insert(m_data.end(), data.cbegin(), data.cend()); });
} catch (std::invalid_argument const& e) {
  throw std::invalid_argument(std::string(e.what()).append(" in Channel::push_back<T>(int, std::vector<T>)"));
}

template <typename T> void Channel::check_type() const {
  if (m_record.type != Data::get_type<T>()) throw std::invalid_argument("type T does not match internal type in Channel");
}
EN

回答 2

Code Review用户

回答已采纳

发布于 2016-06-24 20:58:36

在我看来,首先突出的是对SFINAE的过度使用和滥用:

代码语言:javascript
复制
// SFINAE Catch-All Function. Prints descriptive error message in case of type deduction failure.
template <typename T> auto Channel::push_back(T)
-> std::enable_if_t<!std::is_arithmetic<T>::value> {
  static_assert(std::is_arithmetic<T>::value, "type T must be arithmetic");
}

上面的代码没有任何意义。首先,这种enable_if的使用只有在为算术类型启用了签名push_back(T)的另一个函数时才有意义;但您没有这样做。您在这里实现的是一个函数push_back(T),它在T是算术时不参与过载解析,而在T不是算术时静态断言;也就是说,它完全无用。

可能是个错误,你想要有一个frame参数-

代码语言:javascript
复制
template <typename T> auto Channel::push_back(int /*frame*/, T /*datum*/)
-> std::enable_if_t<!std::is_arithmetic<T>::value> {
  static_assert(std::is_arithmetic<T>::value, "type T must be arithmetic");
}

这样,这个过载就会接收到push_back(frame, datum)的调用,而这些调用没有被其他SFINAE超载的签名所捕获。但是,在这种情况下,SFINAE并没有给你买任何东西。SFINAE的目的是使函数消失,而不是使它们出错。如果您只想让它们出错,这就是static_assert的目的。删除此重载,并将另一个重载替换为

代码语言:javascript
复制
template <typename T> auto Channel::push_back(int frame, T datum)
try {
  static_assert(std::is_arithmetic_v<T>, "type T must be arithmetic");  // Ta-da!
  this->check_type<T>();
  m_frames.emplace_back(frame);
  this->get_data([&datum = datum](auto& m_data) { m_data.push_back(std::move(datum)); });
} catch (std::invalid_argument const& e) {
  throw std::invalid_argument(std::string(e.what()).append(" in Channel::push_back<T>(int, T)"));
}

正如@JanKorous所说,在C++中看到函数-try-块是非常罕见的,除非可能在构造函数中(因为函数-try-块还捕获成员初始化程序-列表中抛出的表达式中的异常)。从上下文来看,我猜想您这样做是为了使您的函数体能够有一个return,从而使静态分析更容易。但是是的,我会避免在生产代码库中使用函数块,仅仅是因为它会混淆所有跟踪你的人(可能在六个月后也包括你自己)。

你的缩进风格有点特别。考虑使用四个空格缩进(而不是两个空格缩进),而不是把那么多东西放在函数头行中。就个人而言,我更喜欢

代码语言:javascript
复制
template<typename T>
auto X::foo(T t) const
    -> decltype(t+1)
{
    return t+1;
}

而你似乎更喜欢

代码语言:javascript
复制
template<typename T> auto X::foo(T t) const -> decltype(t+1) {
    return t+1;
}

在我看来,这仅仅是一条线上太多的东西。

这不是每个源列的问题,而是每个源行的想法问题。每个源行的一个想法是正确的,IMHO。“这是一个模板.这是它的签名.哦,它有一个令人惊讶的/SFINAE会返回的类型.好吧,这是函数体。”

get_data中,您可以反复重复该模式。

代码语言:javascript
复制
static_cast<DataT<SOMETHING>*>(m_data.get())->data

这是深嵌套的,因此很难解析。如果您将它分离成一个助手方法,会怎样?

代码语言:javascript
复制
template<typename T>
auto& get_data() const
{
    return static_cast<DataT<T> *>(m_data.get())->data;
}

?然后你可以简单地写

代码语言:javascript
复制
template <typename Function>
auto Channel::get_data(Function func)
{
    switch (m_record.type) {
        case Data::Type::Double: return func(get_data<double>());
        case Data::Type::Float: return func(get_data<float>());
        case Data::Type::Int: return func(get_data<int>());
        case Data::Type::Short: return func(get_data<short>());
        case Data::Type::Char: return func(get_data<char>());
        default: throw std::logic_error("invalid Data::Type set in Channel");
    }
}

实际上,您应该阅读X宏,并考虑上面的代码是否更具可读性和可维护性(特别是如果您想要支持无符号类型、long long等时会发生什么)。如果你把它写成

代码语言:javascript
复制
template <typename Function>
auto Channel::get_data(Function func)
{
    switch (m_record.type) {
#define X(ENUM, TYPE) \
        case Data::Type::ENUM: return func(get_data<TYPE>());
#include "supported_types.ipp"
#undef X
        default: throw std::logic_error("invalid Data::Type set in Channel");
    }
}
代码语言:javascript
复制
  // Data Interface Functions.
  template <typename T> decltype(DataT<T>::data) const& data() const;
  template <typename T> T const& datum(int frame, int offset) const;
  template <typename Function> auto data(Function func) const;
  template <typename Function> auto datum(int frame, int offset, Function func) const;

拥有同时命名为datadatum的方法(以及上面的get_data)是自找麻烦。每个名字都有两个签名,这实际上是个麻烦。在这里传递Function func的意义是什么?当您只需要让客户端代码对非Function-taking方法的返回值调用它们的func时,又有什么意义呢?

代码语言:javascript
复制
decltype(m_frames) const& frames() const { return m_frames;        }

如果简单地写下来,这会更清楚。

代码语言:javascript
复制
const auto& frames() const { return m_frames; }

我不确定,提供一个非const版本也是有意义的。

而不是

代码语言:javascript
复制
  enum struct Type : char {
    Double = 'd'
  , Float  = 'f'
  , Int    = 'i'
  , Short  = 's'
  , Char   = 'c'
  };

考虑直接使用typeid;如果您可以这样说的话,它将简化大量的代码

代码语言:javascript
复制
typeid(char)

而不是

代码语言:javascript
复制
Data::Type::Char

诸若此类。此外,您可以更容易地扩展代码以处理例如long long,而不必考虑Data::Type::LongLong的枚举器值(还是Data::Type::Longlong?)应该是“L”、“L”或者..。

boost::any的S type()方法直接处理typeids,它一直对我很有用。

我怀疑您对std::unique_ptr的需求;似乎您在堆中添加了一层您并不真正需要的东西。但是我还没有仔细观察过,所以我不能百分之百地确定。

票数 1
EN

Code Review用户

发布于 2016-06-23 21:48:08

非常好的代码,这是鼓舞人心的阅读!我没几个小屁孩。

接口

我更愿意在公共接口中使用特定类型,而不是decltypeauto。我想用户可能会很感激。

在这里。

代码语言:javascript
复制
// Channel Interface Functions.
decltype(m_frames) const& frames() const { return m_frames;        }

或者在这里

代码语言:javascript
复制
template <typename Function> auto data(Function func) const;    

数据::通道中的类型转换::创建

在很大程度上,这段代码是向Data::get_type()的反向转换,我可能会将该逻辑分离出来。

代码语言:javascript
复制
switch (record.type) {
case Data::Type::Double : return {std::make_unique<DataT<double>>(), std::move(record)};
case Data::Type::Float  : return {std::make_unique<DataT<float >>(), std::move(record)};
case Data::Type::Int    : return {std::make_unique<DataT<int   >>(), std::move(record)};
case Data::Type::Short  : return {std::make_unique<DataT<short >>(), std::move(record)};
case Data::Type::Char   : return {std::make_unique<DataT<char  >>(), std::move(record)};
default : throw std::invalid_argument("invalid Data::Type in Channel::create(Data::Type)");
}

函数尝试块

对于非构造函数的使用来说,这是一件很奇特的事情。这里没有什么问题,但是维护代码的人可能会感到惊讶。不过,这是不必要的。

代码语言:javascript
复制
template <typename T> decltype(DataT<T>::data) const& Channel::data() const try {
  check_type<T>();
  return static_cast<DataT<T> const*>(m_data.get())->data;
} catch (std::invalid_argument const& e) {
  throw std::invalid_argument(std::string(e.what()).append(" in Channel::data<T>()"));
}

可以重写为

代码语言:javascript
复制
template <typename T> decltype(DataT<T>::data) const& Channel::data() const {
    try {
      check_type<T>();
      return static_cast<DataT<T> const*>(m_data.get())->data;
    } catch (std::invalid_argument const& e) {
      throw std::invalid_argument(std::string(e.what()).append(" in Channel::data<T>()"));
    }
}

我承认这是相当主观的。

票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/132877

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档