Template class static string member not initialized correctly

134 Views Asked by At

I am trying to create a simple wrapper around glib2.0's GVariant.

I imagined to have a templated class that would be used to derive a format string of GVariant's type:

template <typename T>
struct Object
{
    static const std::string format_string;
};

For now I have hard-coded basic integral types and string and determined the way to derive arrays and dictionaries as follows:

// Integral types
template <>
const std::string Object<uint8_t>::format_string{"y"};
...

// String
template <>
const std::string Object<std::string>::format_string{"s"};

// Array
template <typename T>
struct Object<std::vector<T>>
{
    static const std::string format_string;
};
template <typename T>
const std::string Object<std::vector<T>>::format_string{
  "a" + Object<T>::format_string};

// Dictionary
template <typename K, typename V>
struct Object<std::map<K, V>>
{
    static const std::string format_string;
};
template <typename K, typename V>
const std::string Object<std::map<K, V>>::format_string{
  "{" + Object<K>::format_string + Object<V>::format_string + "}"};

For tuple i use the following string deduction method:

template <typename T, typename... Ts>
std::string derive_string()
{
    if constexpr (sizeof...(Ts)) {
        return Object<T>::format_string + derive_string<Ts...>();
    } else {
        return Object<T>::format_string;
    }
}

// Tuple
template <typename... Ts>
struct Object<std::tuple<Ts...>>
{
    static const std::string format_string;
};

template <typename... Ts>
const std::string Object<std::tuple<Ts...>>::format_string{
  "(" + derive_string<Ts...>() + ")"};

However, when I try to debug-print the format_string member of each Object class

using IntArray = std::vector<int>;
using IntStrMap = std::map<int, std::string>;
using Int_Double_Bool = std::tuple<int, double, bool>;
using Int_IntStrMap_Bool = std::tuple<int, IntStrMap, bool>;
using Int_IntDoubleMap = std::tuple<int, std::map<int, double>>;

std::cout << "bool type:\n  " << Object<bool>::format_string
          << "\nuint8_t type:\n  " << Object<uint8_t>::format_string
          << "\nint16_t type:\n  " << Object<int16_t>::format_string
          << "\nuint16_t type:\n  " << Object<uint16_t>::format_string
          << "\nint32_t type:\n  " << Object<int32_t>::format_string
          << "\nuint32_t type:\n  " << Object<uint32_t>::format_string
          << "\nint64_t type:\n  " << Object<int64_t>::format_string
          << "\nuint64_t type:\n  " << Object<uint64_t>::format_string
          << "\ndouble type:\n  " << Object<double>::format_string
          << "\nstring type:\n  " << Object<std::string>::format_string
          << "\n[int] type\n  " << Object<IntArray>::format_string
          << "\n{int: str} type\n  " << Object<IntStrMap>::format_string
          << "\n(int, double, bool) type\n  "
          << Object<Int_Double_Bool>::format_string
          << "\n(int, {int: str}, bool) type\n  "
          << Object<Int_IntStrMap_Bool>::format_string
          << "\n(int, {int: double}) type\n  "
          << Object<Int_IntDoubleMap>::format_string;
          << std::endl;

I get the following:

bool type:
  b
uint8_t type:
  y
int16_t type:
  n
uint16_t type:
  q
int32_t type:
  i
uint32_t type:
  u
int64_t type:
  x
uint64_t type:
  t
double type:
  d
string type:
  s
[int] type
  ai
{int: str} type
  {is}
(int, double, bool) type
  (idb)
(int, {int: str}, bool) type
  (i{is}b)
(int, {int: double}) type
  (i)

As it is seen from the last two object printouts, the tuple that includes types that were used somewhere else ({int: str}) is derived correctly, while the one that does not ({int: double}), are not.

What am I doing wrong here?

2

There are 2 best solutions below

6
On BEST ANSWER

With C++17, you might simply do:

template <typename... Ts>
const std::string Object<std::tuple<Ts...>>::format_string{
  "(" + (Object<Ts>::format_string + ... + "") + ")"};

which solves the issue with gcc Demo

But problems still exists for clang (even for std::vector<int>).

I suspect Static Initialization Order Fiasco.

Using functions instead of previous/cached results works for both compilers:

std::string make_string(std::type_identity<bool>) { return "b"; }
std::string make_string(std::type_identity<uint8_t>) { return "y"; }
std::string make_string(std::type_identity<uint16_t>) { return "n"; }
std::string make_string(std::type_identity<int16_t>) { return "q"; }
std::string make_string(std::type_identity<int32_t>) { return "i"; }
std::string make_string(std::type_identity<uint32_t>) { return "u"; }
std::string make_string(std::type_identity<int64_t>) { return "x"; }
std::string make_string(std::type_identity<uint64_t>) { return "t"; }
std::string make_string(std::type_identity<double>) { return "d"; }
std::string make_string(std::type_identity<std::string>) { return "s"; }

template <typename T>
std::string make_string(std::type_identity<std::vector<T>>)
{
    return "a" + make_string(std::type_identity<T>{});
}

template <typename K, typename V>
std::string make_string(std::type_identity<std::map<K, V>>)
{
    return  "{" + make_string(std::type_identity<K>{})
                + make_string(std::type_identity<V>{}) + "}";
}

template <typename... Ts>
std::string make_string(std::type_identity<std::tuple<Ts...>>)
{
    return  "(" + (make_string(std::type_identity<Ts>{}) + ... + "") + ")";
}

template <typename T>
struct Object
{
    static const std::string format_string;
};

template <typename T>
const std::string Object<T>::format_string = make_string(std::type_identity<T>{});

Demo

1
On

It is because you didn't define the order of initializing format_string variables properly. The following example shows the principle.

...
extern string a;
string b1 = a + "b";
string a = "a";
string b2 = a + "b";

int main()
{
    // At this point, b2 will be "ab", but b1 will be "b".
    ...
}

In your problem, it's hard to define the order because there are many possible combinations of types. So use static methods instead of static member variables for such problems.