Search code examples
c++templatesclass-design

Combining typesafe code with runtime decisions


I am in the process of rewriting some existing code - where previously, all answer information was stored in a string array in memory. Based on the datatype, the data was transformed in various places. Below is a quick mock up of the setup I am aiming for. Essentially you have some questions - and the structure of the answers stored in the database depends on the datatype. Generally I avoid dealing with void*, and casting them to an appropriate type - but I couldn't find a better solution that would allow me to run generic code (by means of lambdas), or be specific if the datatype is known. Templated classes won't help in this case, as all the answers need to be stored in the same vector (as some arithmetic are applied to all answers based on predefined rules).

Any advice is appreciated.

#include <vector>
#include <memory>


struct AddressData
{
    wchar_t Line1[50];
    wchar_t Line2[50];
    long CountrySeqNo;

    AddressData()
    {
        memset(this, 0, sizeof(*this));
    };
};

struct GenericData
{
    wchar_t value[200];

    GenericData()
    {
        memset(this, 0, sizeof(*this));
    };
};

enum class DataType
    : short
{
    GENERIC,
    ADDRESS
};

class AnswerBase
{
protected:
    const void* const data;
    const DataType dataType;

protected:
    AnswerBase(const DataType datatype, const void* const _data)
        : dataType(datatype), data(data)
    {
        if (data == nullptr)
            throw std::exception("Data may not be initialized as NULL");
    };

public:
    /*
        Some generic methods here that would apply logic by means of lambdas etc - these would be overwritten in the derived classes
    */

    template<typename T> const T& GetData() { static_assert(false, "The given type is not supported"); };

    template<> 
    const GenericData& GetData() 
    { 
        if (DataType::GENERIC != dataType)
            throw std::exception("The requested type does not match the value that initialised data");

        return *static_cast<const GenericData* const>(data);
    };
    template<>
    const AddressData& GetData()
    {
        if (DataType::ADDRESS != dataType)
            throw std::exception("The requested type does not match the value that initialised data");

        return *static_cast<const AddressData* const>(data);
    };


};


class AddressAnswer
    : public AnswerBase
{
public:
    AddressAnswer()
        : AnswerBase(DataType::ADDRESS, &answer)
    {
    };

protected:
    AddressData answer;
};


class GenericAnswer
    : public AnswerBase
{
public:
    GenericAnswer()
        : AnswerBase(DataType::GENERIC, &answer)
    {
    };

protected:
    GenericData answer;
};


int main()
{
    std::vector<std::shared_ptr<AnswerBase>> answers;
    answers.push_back(std::make_shared<GenericAnswer>());
    answers.push_back(std::make_shared<AddressAnswer>());


    // In some parts of code - interact with generic methods without needing to check the underlying data type
    // ....
    // ....

    // In parts of code where we know we are dealing with a given type - like saving to a DB


    auto val1 = answers[0]->GetData<GenericData>().value;
    auto val2 = answers[1]->GetData<AddressData>().Line1;

    // this will give a runtime failure
    //auto val3 = answers[0]->GetData<AddressData>().Line1;


    return 0;
}

Solution

  • variant is the clean way to do this. Store it in the parent.

    Alternatively, provide a variant<A,B> GetData() in the parent. Now visiting is encapsulated in the variant returned. The parent stores the data.

    Alternatively, provide a virtual variant<A,B> GetData() = 0. The child type returns the data, either A or B, in the variant in question.

    Alternatively, write virtual A* GetA() = 0; virtual B* GetB() = 0;. Then maybe write a template method called GetData<T> such that GetData<A>() calls GetA, etc.

    Alternatively, write virtual A* Get(tag_t<A>) = 0; virtual B* Get(tag_t<B>)=0;, where

    template<class T>
    struct tag_t {
      using type=T;
      constexpr tag_t(){}
    };
    template<class T>
    constexpr tag_t<T> tag{};
    

    is a tag used for dispatching. Now you can call the right virtual interface by doing a Get(tag<AddressData>).

    In these virtual cases, the data is stored in the derived type.