警告:很多代码。如果太多,请主要关注channel.h和channel.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来访问数据。请告诉我你的想法。非常感谢!
#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"; });
}#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#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#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/** 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");
}发布于 2016-06-24 20:58:36
在我看来,首先突出的是对SFINAE的过度使用和滥用:
// 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参数-
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的目的。删除此重载,并将另一个重载替换为
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,从而使静态分析更容易。但是是的,我会避免在生产代码库中使用函数块,仅仅是因为它会混淆所有跟踪你的人(可能在六个月后也包括你自己)。
你的缩进风格有点特别。考虑使用四个空格缩进(而不是两个空格缩进),而不是把那么多东西放在函数头行中。就个人而言,我更喜欢
template<typename T>
auto X::foo(T t) const
-> decltype(t+1)
{
return t+1;
}而你似乎更喜欢
template<typename T> auto X::foo(T t) const -> decltype(t+1) {
return t+1;
}在我看来,这仅仅是一条线上太多的东西。
这不是每个源列的问题,而是每个源行的想法问题。每个源行的一个想法是正确的,IMHO。“这是一个模板.这是它的签名.哦,它有一个令人惊讶的/SFINAE会返回的类型.好吧,这是函数体。”
在get_data中,您可以反复重复该模式。
static_cast<DataT<SOMETHING>*>(m_data.get())->data这是深嵌套的,因此很难解析。如果您将它分离成一个助手方法,会怎样?
template<typename T>
auto& get_data() const
{
return static_cast<DataT<T> *>(m_data.get())->data;
}?然后你可以简单地写
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等时会发生什么)。如果你把它写成
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");
}
} // 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;拥有同时命名为data和datum的方法(以及上面的get_data)是自找麻烦。每个名字都有两个签名,这实际上是个麻烦。在这里传递Function func的意义是什么?当您只需要让客户端代码对非Function-taking方法的返回值调用它们的func时,又有什么意义呢?
decltype(m_frames) const& frames() const { return m_frames; }如果简单地写下来,这会更清楚。
const auto& frames() const { return m_frames; }我不确定,提供一个非const版本也是有意义的。
而不是
enum struct Type : char {
Double = 'd'
, Float = 'f'
, Int = 'i'
, Short = 's'
, Char = 'c'
};考虑直接使用typeid;如果您可以这样说的话,它将简化大量的代码
typeid(char)而不是
Data::Type::Char诸若此类。此外,您可以更容易地扩展代码以处理例如long long,而不必考虑Data::Type::LongLong的枚举器值(还是Data::Type::Longlong?)应该是“L”、“L”或者..。
boost::any的S type()方法直接处理typeids,它一直对我很有用。
我怀疑您对std::unique_ptr的需求;似乎您在堆中添加了一层您并不真正需要的东西。但是我还没有仔细观察过,所以我不能百分之百地确定。
发布于 2016-06-23 21:48:08
非常好的代码,这是鼓舞人心的阅读!我没几个小屁孩。
我更愿意在公共接口中使用特定类型,而不是decltype和auto。我想用户可能会很感激。
在这里。
// Channel Interface Functions.
decltype(m_frames) const& frames() const { return m_frames; }或者在这里
template <typename Function> auto data(Function func) const; 在很大程度上,这段代码是向Data::get_type()的反向转换,我可能会将该逻辑分离出来。
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)");
}对于非构造函数的使用来说,这是一件很奇特的事情。这里没有什么问题,但是维护代码的人可能会感到惊讶。不过,这是不必要的。
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> 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>()"));
}
}我承认这是相当主观的。
https://codereview.stackexchange.com/questions/132877
复制相似问题