0

I know the title is not meaningful, couldn't find anything better.

I need to provide a C++ interface to an SQlite table, where I can store key/value/type configuration settings, such as

    Key     |   Value    |   Type
PATH        | /path/to/  |  STRING
HAS_FEATURE |    Y       |  BOOLEAN
REFRESH_RATE|    60      |  INTEGER

For simplicity and flexibility purposes the datamodel hosts the values as strings but provides a column to retain the original data type.

This is how I have imagined a client to call such c++ interface.

Configuration c;
int refreshRate = c.get<int>("REFRESH_RATE");

// Next line throws since type won't match
std::string refreshRate = c.get<std::string>("REFRESH_RATE");

This is how I have imagined implementing it (I know the code won't compile as is, consider it as pseudo c++, I'm more questioning the design than the syntax here)

class Parameter
{
    public:
        enum KnownTypes 
        {
            STRING = 0,
            BOOLEAN,
            INTEGER,
            DOUBLE,
            ...
        }

        std::string key;
        std::string value;
        KnownTypes type;
}

class Configuration 
{
    public:
        template<class RETURNTYPE>
        RETURNTYPE get(std::string& key)
        {
            // get parameter(eg. get cached value or from db...)
            const Parameter& parameter = retrieveFromDbOrCache(key);

            return <parameter.type, RETURNTYPE>getImpl(parameter);
        }

    private:
        template<int ENUMTYPE, class RETURNTYPE>
        RETURNTYPE getImpl(const Parameter& parameter)
        {
            throw "Tthe requested return type does not match with the actual parameter's type"; // shall never happen
        }

        template<Parameter::KnownTypes::STRING, std::string>
        std::string getImpl(const Parameter& parameter)
        {
            return parameter.value;
        }

        template<Parameter::KnownTypes::BOOLEAN, bool>
        std::string getImpl(const Parameter& parameter)
        {
            return parameter.value == "Y";
        }

        template<Parameter::KnownTypes::INTEGER, int>
        int getImpl(const Parameter& parameter)
        {
            return lexical_cast<int>(parameter.value)
        }

        // and so on, specialize once per known type
};

Is that a good implementation ? Any suggestions on how to improve it ?

I know I could have specialized the public get directly per return type, but I would have duplicated some code in each template specialization (the type consistency check as well as the parameter retrieval)

codeJack
  • 2,423
  • 5
  • 24
  • 31
  • 1
    I would just use a different name for the different types, no need for any template stuff here, `getInt(..)`, `getString(..)`, `getBool()` etc. easier.. – Nim May 30 '17 at 12:13

2 Answers2

2

Your approach will fail badly if you try to implement it out! Problem is:

return <parameter.type, RETURNTYPE>getImpl(parameter);

or with correct C++ syntax:

return getImpl<parameter.type, RETURNTYPE>(parameter);

Template parameters require to be compile time constants, which parameter.type is not! So you would have to try something like this:

switch(parameter.type)
{
case STRING:
    return getImpl<STRING, RETURNTYPE>(parameter);
//...
}

Does not look like you gained anything at all, does it?

You might try the other way round, though, specialising the getter itself:

public:
    template<class RETURNTYPE>
    RETURNTYPE get(std::string const& key);

    template<>
    std::string get<std::string>(std::string const& key)
    {
        return getImpl<STRING>(key);
    }
    template<>
    int get<int>(std::string const& key)
    {
        return lexical_cast<int>(getImpl<STRING>(key));
    }

private:
    template<KnownTypes Type>
    std::string getImpl(std::string const& key)
    {
        Parameter parameter = ...;
        if(parameter.type != Type)
            throw ...;
        return parameter.value;
    }

Or without templates (referring Nim's comment...):

public:
    int getInt(std::string const& key)
    {
        return lexical_cast<int>(getImpl(STRING, key));
    }

private:
    inline std::string getImpl(KnownTypes type, std::string const& key)
    {
        Parameter parameter = ...;
        if(parameter.type != type)
            throw ...;
        return parameter.value;
    }

One change you might have noticed: I fixed constness for your parameters...

Side note: template specialisations as above are not allowed at class scope (above is written for shortness). In your true code, you have to move the specialisations out of the class:

struct S { template<typename T> void f(T t); };

template<> void S::f<int>(int t) { }
Aconcagua
  • 24,880
  • 4
  • 34
  • 59
  • Thanks for the answer. Do you see any advantage other than cosmetic difference in using templates against defining a dedicated method per type - Nim's comment - ? – codeJack May 30 '17 at 12:54
  • 1
    @codeJack In this specific case - no. Written as above, completely defined within the header and within the class, both template and ordinary function should be inlined anyway, so in the compiled code, there shouldn't be any difference at all any more... – Aconcagua May 30 '17 at 12:59
  • why did you change the constness having it at reference level ? – codeJack May 31 '17 at 07:43
  • @codeJack Well, you are not going to modify the string - are you? So by having a const reference, you can pass both const and non-const strings to your function. If your function does not modify the object (in the sense of its external representation), it is even a good idea to make the function itself const. This would allow to use the function even on a const object... Counter example: try to call append on a const std::string... – Aconcagua May 31 '17 at 07:53
  • https://stackoverflow.com/questions/3694630/c-const-reference-before-vs-after-type-specifier – codeJack May 31 '17 at 08:36
  • There is no actual difference, it's mostly cosmetic – codeJack May 31 '17 at 08:36
  • @codeJack Well, before, after - yes, no difference. I personally prefer after (e. g. `char const* const`, so const *always* refers to what precedes it...). But that's not the point - the point is: I added const where there wasn't one before... – Aconcagua May 31 '17 at 08:40
1

In addition to the accepted answer I would like to add a demo with a little difference of verifying the correctness of the type without boilerplatting the code over all template specializations. As well correcting the explicit-template specialization in the class scope that is not allowed.

class Parameter {
public:
  enum KnownTypes { STRING = 0, BOOLEAN, INTEGER, DOUBLE };

  std::string key;
  std::string value;
  KnownTypes type;
};

class Configuration {
public:
  template <class RETURNTYPE>
  RETURNTYPE get(std::string const& key) {
    // get parameter(eg. get cached value or from db...)
    std::map<std::string, Parameter> map{
      {"int", Parameter{"int", "100", Parameter::KnownTypes::INTEGER}},
      {"string", Parameter{"string", "string_value", Parameter::KnownTypes::STRING}},
      {"throwMe", Parameter{"throwMe", "throw", Parameter::KnownTypes::DOUBLE}},
      {"bool", Parameter{"bool", "Y", Parameter::KnownTypes::BOOLEAN}}};
    const Parameter& parameter = map.at(key);

    bool isMatchingType = false;
    switch (parameter.type) {
    case Parameter::STRING:
      isMatchingType = std::is_same<RETURNTYPE, std::string>::value;
      break;
    case Parameter::BOOLEAN:
      isMatchingType = std::is_same<RETURNTYPE, bool>::value;
      break;
    case Parameter::INTEGER:
      isMatchingType = std::is_same<RETURNTYPE, int>::value;
      break;
    case Parameter::DOUBLE:
      isMatchingType = std::is_same<RETURNTYPE, double>::value;
      break;
    };

    if (!isMatchingType)
      throw "Tthe requested return type does not match with the actual parameter's type";

    return getImpl<RETURNTYPE>(parameter);
  }

private:
  template <class RETURNTYPE>
  RETURNTYPE getImpl(const Parameter& parameter);
};

template <>
std::string Configuration::getImpl<std::string>(const Parameter& parameter) {
  return parameter.value;
}

template <>
bool Configuration::getImpl<bool>(const Parameter& parameter) {
  return parameter.value == "Y";
}

template <>
int Configuration::getImpl<int>(const Parameter& parameter) {
  return std::stoi(parameter.value);
}

int main() {
  Configuration conf;
  cerr << conf.get<int>("int") << endl;
  cerr << conf.get<bool>("bool") << endl;
  cerr << conf.get<string>("string") << endl;
  cerr << conf.get<string>("throwMe") << endl;

  return 0;
}
Yuki
  • 3,857
  • 5
  • 25
  • 43
  • Thanks, I like this solution, I think it is the better factorized! – codeJack May 31 '17 at 07:17
  • That's the switch-case stuff I did not implement out because of having preferred to avoid it... I recommend to add a default to the switch instruction, though (might throw, too). Some coding standards even mandate it (not 100% sure, but I think e. g. MISRA, too). Good point: template specialisations within class scope - sure, I am well aware of, wrote that for shortness only. Seems, though, that I should at least have mentioned it... – Aconcagua May 31 '17 at 08:50
  • I know one practical reason why to use the switch statement with enums: when you add a new item to the enum, compiler returns an error if you forgot to add this item to the switch. – Yuki May 31 '17 at 09:29