I would like to have my variadic template class reviewed.
First some explanation what it should do:
I'm writing an application where I read in blocks of data.
I know before i read in the data which formats to expect.
For example:
ID123.EnableValve := FALSE;
ID123.Position1 := 0;
ID123.Position2 := 2;
ID123.Position3 := 9;
Or:
UDT30.EnableValve := FALSE;
UDT30.This := 0;
UDT30.is := L#2;
UDT30.an := 9;
UDT30.Example := TRUE;
These data blocks share the following similarities:
- Each line starts with an id (e.g.
UDT30orID123). - This Device Id must be the same on each line. Otherwise we read a corrupt data block.
- Then a type name follows (e.g.
EnableValveorPosition3). - The type names are unique. That means in one data block there is never
the same type twice (e.g.
Position1does not occur two times). - Then always the string
:=follows. - Add the end the value follows (e.g.
TRUE,0,L#). - The count of lines is flexible (e.g. length =
4or5).
I want to do the following with the datablocks:
- Reading in datablocks (from
std::istream) - Access a specific row and modify its value(e.g.
ID123.EnableValve := FALSE;becomesID123.EnableValve := TRUE; - Writing the datablock back (to
std::ostream)
Also it must be possible to:
- construct a new datablock out of Types
- write the datablock (to
std::osstream)
I want to inherit from this template and restrict the use of set and get for the rows. The inherited classes can decide with the protected set / get which lines they want to modify and which are just read only.
I used Google Test with Visual 2017 to create unit tests for the template. Here you can see what the template can do.
Variadic_templatesTest.cpp
#include "pch.h"
#include "..\variadic_templates\Fake_types.h"
// hack to test protected methods in variadic datablock
// is there a better way??
#define protected public
#include "..\variadic_templates\Variadic_datablock.h"
#include <sstream>
TEST(Variadic_templatesTest, DefaultConstructor) {
Variadic_datablock t;
EXPECT_EQ(t.get_id(), std::string{});
}
TEST(Variadic_templatesTest, Constructor) {
std::string id{ "ID123" };
EnableValve enableValve{ "TRUE" };
Position1 position1{ "0" };
Position2 position2{ "2" };
Position3 position3{ "3" };
Variadic_datablock<EnableValve, Position1, Position2, Position3> t{
id,
enableValve,
position1,
position2,
position3
};
EXPECT_EQ(t.get_id(), id);
EXPECT_EQ(t.get_element<EnableValve>().m, enableValve.m);
EXPECT_EQ(t.get_element<Position1>().m, position1.m);
EXPECT_EQ(t.get_element<Position2>().m, position2.m);
EXPECT_EQ(t.get_element<Position3>().m, position3.m);
}
TEST(Variadic_templatesTest, SetElement) {
std::string id{ "ID123" };
EnableValve enableValve{ "TRUE" };
Variadic_datablock<EnableValve> t{
id,
enableValve,
};
EXPECT_EQ(t.get_element<EnableValve>().m, enableValve.m);
enableValve.m = "FALSE";
t.set_element(enableValve);
EXPECT_EQ(t.get_element<EnableValve>().m, enableValve.m);
}
TEST(Variadic_templatesTest, TestIOstream) {
std::string input = {
R"( ID123.EnableValve := FALSE;
ID123.Position1 := 0;
ID123.Position2 := 2;
ID123.Position3 := 9;
)"
};
std::istringstream ist{ input };
Variadic_datablock<EnableValve, Position1, Position2, Position3> t;
ist >> t;
EXPECT_EQ(t.get_id(), "ID123");
EXPECT_EQ(t.get_element<EnableValve>().m, "FALSE");
EXPECT_EQ(t.get_element<Position1>().m, "0");
EXPECT_EQ(t.get_element<Position2>().m, "2");
EXPECT_EQ(t.get_element<Position3>().m, "9");
std::ostringstream ost;
ost << t;
EXPECT_EQ(ost.str(), input);
}
Variadic_datablock.h
#pragma once
#include "Read_from_line.h"
#include <tuple>
#include <iostream>
#include <string>
#include <vector>
#include <typeinfo>
template<typename ...T>
class Variadic_datablock
/*
Requires elements in T... are unique (not repeat of types)
*/
{
public:
Variadic_datablock() = default;
explicit Variadic_datablock(std::string id, T... args)
:m_id{ std::move(id) },
m_data{ std::move((std::tuple<T...>(args...))) }
{
}
std::string get_id() const
{
return m_id;
}
protected:
template<typename Type>
Type get_element() const
{
return std::get<Type>(m_data);
}
template<typename Type>
void set_element(Type a)
{
std::get<Type>(m_data) = a;
}
std::tuple<T...> get_data() const
{
return m_data;
}
private:
std::string m_id{};
std::tuple<T...> m_data{};
template<typename ...T>
friend std::ostream& operator<<(std::ostream& os,
const Variadic_datablock<T...>& obj);
template<typename ...T>
friend std::istream& operator>>(std::istream& is,
Variadic_datablock<T...>& obj);
};
template<class Tuple, std::size_t n>
struct Printer {
static std::ostream& print(
std::ostream& os, const Tuple& t, const std::string& id)
{
Printer<Tuple, n - 1>::print(os, t, id);
auto type_name =
extract_type_name(typeid(std::get<n - 1>(t)).name());
os << " " << id << "." << type_name << " := "
<< std::get<n - 1>(t) << "; " << '\n';
return os;
}
};
template<class Tuple>
struct Printer<Tuple, 1> {
static std::ostream& print(
std::ostream& os, const Tuple& t, const std::string& id)
{
auto type_name =
extract_type_name(typeid(std::get<0>(t)).name());
os << " " << id << "." << type_name << " := "
<< std::get<0>(t) << "; " << '\n';
return os;
}
};
template<class... Args>
std::ostream& print(
std::ostream& os, const std::tuple<Args...>& t, const std::string& id)
{
Printer<decltype(t), sizeof...(Args)>::print(os, t, id);
return os;
}
template<typename ...T>
std::ostream& operator<<(
std::ostream& os, const Variadic_datablock<T...>& obj)
{
print(os, obj.m_data, obj.m_id);
return os;
}
template<class Tuple, std::size_t n>
struct Reader {
static std::istream& read(
std::istream& is, Tuple& t, std::string& last_id)
{
Reader<Tuple, n - 1>::read(is, t, last_id);
auto id = extract_id(is);
if (!is_expected_id(is, id, last_id)) {
return is;
}
last_id = id;
auto type_name = extract_type_name(is);
auto expected_name =
extract_type_name(typeid(std::get<n - 1>(t)).name());
if (!is_expected_name(is, type_name, expected_name)) {
return is;
}
dischard_fill(is);
// can we do this better and extract into type of
// actual dispatched tuple?
auto s = extract_line_value_type(is);
// prefered: std::get<n - 1>(t) = s but not possible?
std::get<n - 1>(t).insert(s);
return is;
}
};
template<class Tuple>
struct Reader<Tuple, 1> {
static std::istream& read(
std::istream& is, Tuple& t, std::string& last_id)
{
auto id = extract_id(is);
if (!is_expected_id(is, id, last_id)) {
return is;
}
last_id = id;
auto type_name = extract_type_name(is);
auto expected_name =
extract_type_name(typeid(std::get<0>(t)).name());
if (!is_expected_name(is, type_name, expected_name)) {
return is;
}
dischard_fill(is);
// can we do this better and extract into the type of
// actual dispatched tuple?
// typeid(std::get<0>(t)) s = extract_line_value_type(is); ?
auto s = extract_line_value_type(is);
// prefered: std::get<0>(t) = s but not possible?
std::get<0>(t).insert(s);
return is;
}
};
template<class... Args>
std::istream& read(
std::istream& is, std::tuple<Args...>& t, std::string& last_id)
{
Reader<decltype(t), sizeof...(Args)>::read(is, t, last_id);
return is;
}
template<typename ...T>
std::istream& operator>>(std::istream& is,
Variadic_datablock<T...>& obj)
{
std::tuple<T...> tmp{};
read(is, tmp, obj.m_id);
obj.m_data = std::move(tmp);
return is;
}
Read_from_line.h
#pragma once
#include <iosfwd>
#include <string>
std::string extract_id(std::istream& is);
bool is_expected_id(std::istream& is,
const std::string& id, const std::string& expected_id);
std::string extract_type_name(std::istream& is);
bool is_expected_name(std::istream& is,
const std::string_view& name, const std::string_view& expected_name);
void dischard_fill(std::istream& is);
std::string extract_line_value_type(std::istream& is);
std::string erase_whitespace_in_begin(const std::string& s);
std::string extract_type_name(const std::string& typeid_result);
Read_from_line.cpp
#include "Read_from_line.h"
#include <algorithm>
#include <iostream>
#include <sstream>
std::string extract_id(std::istream& is)
{
std::string id; // id e.g. K101 PI108
std::getline(is, id, '.');
id = erase_whitespace_in_begin(id);
return id;
}
bool is_expected_id(std::istream& is,
const std::string& id, const std::string& expected_id)
{
if (expected_id == std::string{}) {
return true;
}
if (id != expected_id) {
is.setstate(std::ios::failbit);
return false;
}
return true;
}
std::string extract_type_name(std::istream& is)
{
std::string type_name; // data e.g. DeviceType
is >> type_name;
return type_name;
}
bool is_expected_name(std::istream& is,
const std::string_view& name, const std::string_view& expected_name)
{
if (name != expected_name) {
is.setstate(std::ios::failbit);
return false;
}
return true;
}
void dischard_fill(std::istream& is)
{
std::string fill; // fill ":="
is >> fill;
}
std::string extract_line_value_type(std::istream& is)
{
std::string value; // value 10
std::getline(is, value, ';');
value = erase_whitespace_in_begin(value);
return value;
}
std::string erase_whitespace_in_begin(const std::string& s)
{
std::string ret = s;
ret.erase(0, ret.find_first_not_of(" \n\r\t"));
return ret;
}
std::string extract_type_name(const std::string& typeid_result)
{
std::string ret = typeid_result;
// case normal name class Test
if (ret.find('<') == std::string::npos) {
std::istringstream ist{ ret };
ist >> ret;
ist >> ret;
return ret;
}
// Case using Test = Test2<struct TestTag>
{
std::istringstream ist{ ret };
std::getline(ist, ret, '<');
std::getline(ist, ret, '>');
}
{
std::istringstream ist2{ ret };
ist2 >> ret; // read name such as struct or class
ist2 >> ret; // get typenameTag
}
ret.erase(ret.find("Tag"));
if (ret.find("EventMask") != std::string::npos) {
std::replace(ret.begin(), ret.end(), '_', '.');
}
return ret;
}
Fake_types.h
#pragma once
/*
Fake types used to test the template class.
*/
#include <iostream>
#include <string>
template<typename Tag>
struct Faketype {
std::string m;
void insert(std::string(s)) {
m = s;
}
};
template<typename Tag>
std::istream& operator>>(std::istream& is, Faketype<Tag>& obj)
{
is >> obj.m;
return is;
}
template<typename Tag>
std::ostream& operator<<(std::ostream& os, const Faketype<Tag>& obj)
{
os << obj.m;
return os;
}
using Position1 = Faketype<struct Position1Tag>;
using Position2 = Faketype<struct Position2Tag>;
using Position3 = Faketype<struct Position3Tag>;
// Only Special case. The file has EventMask.Address1
using EventMask_Address1 = Faketype<struct EventMask_Address1Tag>;
struct EnableValve
{
std::string m;
void insert(std::string(s)) {
m = s;
}
};
std::istream& operator>>(std::istream& is, EnableValve& obj)
{
is >> obj.m;
return is;
}
std::ostream& operator<<(std::ostream& os, const EnableValve& obj)
{
os << obj.m;
return os;
}
Please review, sorted by priority:
The
Variadic_datablock class: Is it a good design? What can be improved? Is it possible to changestd::get<n - 1>(t).insert(s);tostd::get<n - 1>(t) = sin theReader? Please let me know any smells. It's the first time I have used variadic templates. I left some comments in the template which indicate smells, but I don't know how to fix them.The unit test for the
Variadic_datablock class: Are they good test? Easy to understand? Is there something else you would test? Can we ged rid of theprotected/publichack?Read_from_line.hThis file provided helper functions for the overloaded Istream Operator of theVariadic_datablock.
Do not review:
Fake_types.h: It is a helper to emulate some datatypes, because the real datatypes I use in my application are more complicated and I wanted to simplyfy to focus on the template
edit: Let me know if you need additional informations to review this.