#StackBounty: #c++ #template #c++17 #variadic Variadic Datablocks

Bounty: 50

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.

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. UDT30 or ID123).
  • This Device Id must be the same on each line. Otherwise we read a corrupt data block.
  • Then a type name follows (e.g. EnableValve or Position3).
  • The type names are unique. That means in one data block there is never
    the same type twice (e.g. Position1 does 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 = 4 or 5).

I want to do the following with the datablocks:

  1. Reading in datablocks (from std::istream)
  2. Access a specific row and modify its value(e.g. ID123.EnableValve
    := FALSE;
    becomes ID123.EnableValve := TRUE;
  3. Writing the datablock back (to std::ostream)

Also it must be possible to:

  1. construct a new datablock out of Types
  2. write the datablock (to std::osstream)

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_templatesFake_types.h"

// hack to test protected methods in variadic datablock
// is there a better way??
#define protected public

#include "..variadic_templatesVariadic_datablock.h"

#include <sstream>

TEST(Variadic_templatesTest, DefaultConstructor) {

    Variadic_datablock t;
    EXPECT_EQ(t.get_id(), std::string{});
}


template<typename ...T>
class Variadic_datablock_exposed : public Variadic_datablock<T...>
{
public:
    using Variadic_datablock::Variadic_datablock;
};

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(" nrt"));
    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 change std::get<n - 1>(t).insert(s); to std::get<n - 1>(t) = s in the Reader?
    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 the protected / public hack?

  • Read_from_line.h This file provided helper functions for the overloaded Istream Operator of the Variadic_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


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.