3
\$\begingroup\$

In a current project I have implemented a small non-intrusive serialization framework based on type traits. The generic trait template looks like this:

template<typename T>
class Serializable
{
public:
    static const bool isSerializableToString = false;

    static QString toString(const T &t) {
        (void) Error<T>::Trait_does_not_support_serialization_to_strings();
        return QString();
    }

    static T fromString(const QString &s, const T &defaultValue, bool *ok = 0) {
        (void) Error<T>::Trait_does_not_support_serialization_to_strings();
        return defaultValue;
    }

private:
    template<typename I>
    struct Error { };
};

To implement serialization for a certain type, this trait is specialized for this type, the isSerializableToString constant is defined true and the appropriate methods are implemented.

For types that do not support serialization I intended to raise a compile time error in the generic template.

I came along like 1000 solutions to accomplish this just on Stackoverflow. Among others variations, most solutions use static_asserts or SFINAE to various degrees. Yet, while following these solutions and experimenting, I came up with the above solution which seems to work like a charm.

In contrast to most SFINAE-based solutions, it is quite simple to understand (the (void) could even be removed), it does not rely on static_assert or the workarounds provided by, e.g. boost (unfortunately, static_assert is still not supported everywhere I need it) and it generates an error message as compact as

'Trait_does_not_support_serialization_to_strings' is not a member of 'Serializable<SomeType>::Error<SomeType>'
         (void) Error<T>::Trait_does_not_support_serialization_to_strings();

Now given the (imho) simplicity of this approach I am wondering if I am missing some caveat.

Specialization for types

QDate can easily be turned into a string representation by means of its builtin QDate::toString() and QDate::fromString() methods, so the specialization is merely a wrapper around those:

// Header
template<>
class Serializable<QDate>
{
public:
    static const char *const typeName;
    static QString toString(const QDate &value);
    static QDate fromString(const QString &s, const QDate &defaultValue, bool *ok = 0);
};

// Implementation
const char *const Serializable<QDate>::typeName = "date";

QString Serializable<QDate>::toString(const QDate &value) {
    return value.toString(Qt::ISODate);
}

QDate Serializable<QDate>::fromString(const QString &s, const QDate &defaultValue, bool *ok) {
    const QDate value = QDate::fromString(s, Qt::ISODate);
    if (ok)
        *ok = value.isValid();

    return value.isValid() ? value : defaultValue;
}

Other types may require more elaborate implementations. A specialization for bool could generate the strings true and false and it might be tolerant to accept others like yes/no and on/off. The specialization for QColor might convert hexadecimal color strings as used in HTML (#112233).

Usage

Usage should be straightforward:

QDate d = QDate::currentDate();
qDebug() << Serializable<QDate>::toString(d);
\$\endgroup\$
  • \$\begingroup\$ I feel my problem is the other way round: What should I put in the generic trait template? Should I just remove the default (in this case, compile-time-arising) toString() and fromString() methods? If I leave them, what should they return? \$\endgroup\$ – Kamajii Jul 22 '17 at 18:45
  • \$\begingroup\$ PS: If I leave the toString() and fromString() methods in the generic (possibly returning some invalid default value), I fear someone using it on a type that has no specialization, without noticing it. If I remove the methods, I don't have the (kind of self documenting) prototypes anymore. \$\endgroup\$ – Kamajii Jul 22 '17 at 18:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.