Is there a design pattern in C++ that facilitates querying data of different types with a uniform function interface?

Viewed 646

I want to provide a single function declaration/definition that returns the correct type of data based on its input argument. This sounds just like what a function template is for, but more specifically, I would like the function interface to look like:

template<class InT>
RetT getData(InT*);

where,

  • Requirement1: RetT depends on the input type InT and is not necessarily equal to InT.
  • Requirement2: In addition, I want to enforce a common interface for all actual InT types to determine what RetT is. In other words, hopefully, InT should be a base class.

A little bit on the background application. Let's say I have a text processing system for which I can specify various configurations. Some configurations might be flags (i.e., boolean values), like performCompact, addSpacing, etc. Some configurations might be tokens (i.e., string literals), like prefixToPath, surfixToLastName, etc. Some configurations might be of custom data types, like textColorMap, fontFamily, etc.

I want to provide a common function to query the data of each configuration, rather than one function per configuration. And because configuration data are stored in different types, I need this function to return the right type of data depending on what configuration it is used to query.

The primary reason for this design choice is to save the maintenance effort. This query infrastructure is in its own component and different teams are working on different subsets of the configurations in their own components. I want to minimize the maintenance effort so that when a new configuration is added, there is no need to modify the query interface.

Some example code is given below. This satisfies both requirements however I am still wondering if there is a way to avoid a class template for Requirement2.

#include <iostream>
using namespace std;

// In a *.hpp file in component A
template<class T>
auto getData(T* spec) {
    return spec->data();
}

template<class DataT>
class Configuration {
    public:
    virtual DataT data() = 0; 
};

// In some other components source files
class PerformCompact : Configuration<bool> {
    private:
    bool _data;
    public:
    PerformCompact(bool d):_data(d){}
    bool data() override {return _data;}

};

class PrefixToPath : Configuration<string>{
    private:
    string _data;
    public:
    PrefixToPath(string d):_data(d){}
    string data() override {return _data;}
};

// In one application source file
int main()
{
   PerformCompact performCompact(true);
   PrefixToPath prefixToPath("Some string");
   auto pd = getData(&performCompact);
   auto pstr = getData(&prefixToPath);
   cout << pd << endl;
   cout << pstr << endl;

   return 0;
}
2 Answers

It is possible to use SFINAE together with the new metafunctions from <type_traits> check for conditions on types in the return line of a function. If the conditions fail for some function templates, and doesn't fail for the others SFINAE makes sure the compiler moves on and selects the best function template candidate.

This works of course if the conditions on your types can be checked at compile-time. This small example checks if two types are assignable to each other, and then it enables a return type. If the types are not assignable (combinations C with (A,B)), the compilation fails:

#include <type_traits>
#include <cassert>

class A; 
class B; 
class C; 

class A 
{
    public: 
        void operator=(B const& b) {}; 
};  

class B 
{
    public: 
        void operator=(A const& a) {}; 
}; 

class C {}; 

template<class RetT, class InT>
std::enable_if_t
<
    std::is_assignable<InT,RetT>::value, 
    RetT
>
getData(InT*)
{
    RetT result; 

    return result;
};

using namespace std; 

int main()
{
    A* Aptr = new A;
    B* Bptr = new B;
    C* Cptr = new C;

    getData<A>(Bptr); 
    getData<B>(Aptr); 


    // Assignments not there, so the compilation fails. 
    getData<B>(Cptr); 
    getData<C>(Bptr); 
    getData<A>(Cptr); 
    getData<C>(Aptr); 

    delete Aptr; 
    Aptr = nullptr;
    delete Bptr; 
    Bptr = nullptr; 
    delete Cptr; 
    Cptr = nullptr;

    return 0;
};

You can also use if constexpr in the function body if you can use C++17:

template<class RetT, class InT>
RetT
getData(InT*)
{
    if constexpr (std::is_assignable<InT,RetT>::value)
    {
        RetT result; 

        return result;
    }
    else assert(false && "RetT and IntT not assignable.");
};

if constexpr is evaluated at compile time and if true

   RetT result; 

   return result;

is compiled, otherwise the assert statement is compiled. If you try to use the function with C, (A,B) combinations, you get a runtime error:

 RetT getData(InT*) [with RetT = B; InT = C]: Assertion `false && "RetT and IntT not assignable."' failed.

If you want the assertion to be activated at compile time you can use static_assert.

You can define a common requirement for your InT to explicitly define the return type, for example:

class PerformCompact : Configuration<bool> {
    typedef bool DataReturnType;
    ...
}

and then you can define your function as:

template<class T>
typename T::DataReturnType getData(T* spec) {
    return spec->data();
}

If your return type is always the template argument of Configuration then it makes things a bit easier. Instead of explicitly defining it in each derived type, you can just put it in the base class:

template<class DataT>
class Configuration {
    public:
    virtual DataT data() = 0; 
    typedef DataT DataReturnType;
};

If you want to avoid defining the type explicitly, in C++17 you can leave it to the compiler to deduce it on its own, using auto keyword:

template<class T>
auto getData(T* spec) {
    return spec->data();
}
Related