Home > Mobile >  How to implement a get<T>() similar to that in nlohmann/json?
How to implement a get<T>() similar to that in nlohmann/json?

Time:03-30

I'm writing a json library that has the same usage as nlohmann/json. But I'm having trouble understanding nlohmann's get() function. So I implemented a get() myself, but I think that my method is not very good, do you have any good solutions or suggest?

#include <vector>
#include <iostream>
#include <vector>
#include <string>
#include <map>

using namespace std;

bool func(bool) { return 1; }
int func(int) { return 2; }
double func(double) { return 3; }
string func(string&) { return string("4"); }
vector<int> func(vector<int>&) { return { 5 }; }
map<int, int> func(map<int, int>&) {
    map<int, int>a;
    a.emplace(6, 6);
    return a;
}

template <typename T>
T get() {
    static T t;
    return func(t);
}

int main() {
    cout << get<bool>();
    cout << get<int>();
    cout << get<double>();
    cout << get<string>();
    cout << get<vector<int>>()[0];
    cout << get<map<int, int>>()[6];
}

CodePudding user response:

It can be done with a function template.

An MCVE on coliru:

#include <iostream>
#include <string>

template <typename T>
T get() { return T(); }

#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

int main()
{
  DEBUG(std::cout << std::boolalpha << get<bool>() << '\n');
  DEBUG(std::cout << get<int>() << '\n');
  DEBUG(std::cout << get<double>() << '\n');
  DEBUG(std::cout << '"' << get<std::string>() << "\"\n");
}

Output:

std::cout << std::boolalpha << get<bool>() << '\n';
false
std::cout << get<int>() << '\n';
0
std::cout << get<double>() << '\n';
0
std::cout << '"' << get<std::string>() << "\"\n";
""

In C , function overloads must differ by its parameters—only differing return types are not sufficient. (Otherwise, the function call would be ambiguous.)

This changes if function templates are used. Now, the type can be explicitly given in the function call.

I don't know how this fits into OP's infra-structure. Assuming that the rest is defined using templates as well no template function specializations need to be used but they could to the rescue.

Demo on coliru:

#include <iostream>
#include <string>
#include <vector>

template <typename T>
T get() { return T(); }

template <>
bool get() { return true; }

template <>
int get() { return 1; }

template <>
double get() { return 1.23; }

template <>
std::string get() { return "Hello"; }

#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

int main()
{
  DEBUG(std::cout << std::boolalpha << get<bool>() << '\n');
  DEBUG(std::cout << get<int>() << '\n');
  DEBUG(std::cout << get<double>() << '\n');
  DEBUG(std::cout << '"' << get<std::string>() << "\"\n");
}

Output:

std::cout << std::boolalpha << get<bool>() << '\n';
true
std::cout << get<int>() << '\n';
1
std::cout << get<double>() << '\n';
1.23
std::cout << '"' << get<std::string>() << "\"\n";
"Hello"

CodePudding user response:

Your way works, but

  • requires default constructible types (so no void, reference, ...),
  • types which do "nothing" (a RAII object using global mutex would be problematic for example). Even a log might be strange.
  • You should care about conversion/promotion with overloading resolution (get<char> is ambiguous from your types, get<void(*)()> would call func(bool), but fortunately would fail to compile too because of return type)

As alternative:

  • as your set of types seems limited and no extensible,

    • you might used if constexpr:

      template <typename T>
      T get() {
          if constexpr (std::is_same_v<T, bool>) {
              return true;
          } else if constexpr (std::is_same_v<T, int>) {
              return 42;
          } else if constexpr (std::is_same_v<T, double>) {
              return 4.2;
          } else if constexpr (std::is_same_v<T, std::string>) {
              return "Hello world";
          } else if constexpr (std::is_same_v<T, std::vector<int>>) {
              return {4, 8, 15, 16, 23, 42};
          } else if constexpr (std::is_same_v<T, std::map<int, int>>) {
              return {{1, 2}, {3, 4}};
          } else {
              static_assert(always_false<T>::value);
          }
      }
      
    • or (full) specialization:

      template <typename T> T get(); // No impl, to specialize
      
      template <> bool get() { return true; }
      template <> int get() { return 42; }
      template <> double get() { return 4.2; }
      template <> std::string get() { return "Hello world"; }
      template <> std::vector<int> get() { return {4, 8, 15, 16, 23, 42}; }
      template <> std::map<int, int> get() { return {{1, 2}, {3, 4}}; }
      
  • Reusing you idea of dispatching but with a tag. (that allow extensible set of type, and template implementation)

    template <typename T> struct tag {};
    
    bool func(tag<bool>) { return true; }
    int func(tag<int>) { return 42; }
    double func(tag<double>) { return 4.2; }
    std::string func(tag<std::string>) { return string("Hello world"); }
    std::vector<int> func(tag<std::vector<int>>) { return {4, 8, 15, 16, 23, 42}; }
    std::map<int, int> func(tag<std::map<int, int>>) { return {{1, 2}, {3, 4}}; }
    
    #if 0 // Allow extension and template
    template <typename T>
    UserTemplateType<T> func(tag<UserTemplateType<T>>) { return {}; }
    #endif
    
    template <typename T>
    T get() {
        return func(tag<T>{});
    }
    
  • Related