HappyCactus
HappyCactus

Reputation: 2014

How to proper implement type "extension" functions in c++

For a binary protocol library I am developing I make wide use of StrongType<T>, and I'm extending this concept to Enums.

I have a InBitStream object that implements reading from a byte array on a bit-by-bit base, I mean, each field can be read from the array in a fixed and predefined number of bits.

For example, I can read a StrongTyped field that is stored in 5 bits, followed by a 3 other bits in this way:

class SomePacketType {
void readFromStream(DataPacket const &p) {
   InBitStream stream(p);
   auto field1 = makeBitField<5>(mFirstField);  // references to mFirstField, using 5 bits
   auto field2 = makeBitField<3>(mSecondField); // same, 3 bits
   stream >> field1 >> field2;
}

of course, somewhere it is defined this operator

class InBitStream {
  template<int N, typename T, typename TAG>
  friend InBitStream &operator >> (InBitStream &stream, BitField<N,StrongType<T,TAG>> &value) { ...
  }
}

The same is done for Enums. Enums defined this way:

struct XTag {
};
enum class XEnum {
    N = 0, A = 1, B = 2, C = 3, D = 4, 
};
using X = utils::BitAwareEnum<3, XEnum, XTag>;

My idea is now to add a validation function, to check that an enum (or a strong type) is read from the stream and validated according to some special value. For example X should not receive a value > 4.

My intention is to use it this way, supposing there's some ValidatedField concept:

template <???>
InStream &operator >> (InStream &stream, ValidatedField<???> &v) {
  v.validate();
  return stream >> static_cast<some_base_class?>(v);
}

How should ValidatedField be defined? Note that the validation function depends on the templated type. For example for StrongType it would check that it is positive, while for enums it must check against max, min and some internal invalid values.

It is required that the ValidatedField have the same interface as type, so I cannot compose it, and no virtual functions are allowed, because the types are never used polymorphically.

(The pattern should be something like a decorator, perhaps).

Any Idea? Thanks.

Upvotes: 2

Views: 84

Answers (1)

HappyCactus
HappyCactus

Reputation: 2014

I was able to find a good solution then.

This is the ValidatedField class.

template<typename Type, typename Validator>
struct ValidatedType : Type {
    friend InBitStream &operator>>(InBitStream &stream, ValidatedType<Type, Validator> &v)
    {
        stream >> static_cast<Type &>(v);
        Validator::validate(v);
        return stream;
    }
};

This simply create a Tag for the Validated type. The stream operator >> is dispatched using it. The Type template type is used to expose the embedded type, and the Validator type is used like a functor. The Validator must expose a static function validate(Type &const).

In the stream operator implementation, the v argument must be upcast to Type &, to correctly call the same operation on the "decorated" type.

This is how it is used:

   struct XTag {
    };
    enum class XEnum {
        A = 1, B = 2, C = 3, D = 4,
    };
    using XE = utils::BitAwareEnum<4, XEnum, XTag>;
    struct XValidator {
        static void validate(XE const &x)
        {
            if (x.underlyingValue() <= 0 || x.underlyingValue() > 4) {
                throw utils::InBitStream::IllegalValueException("Illegal XE value");
            }
        }
    };
    using X = utils::ValidatedType<XE, XValidator>;
    X x;

What I like of this solution: - I have a "tree" of decorated types whose functionalities can be "plugged-in" as needed

What I don't like of this solution: - The structure of the XValidator is somehow verbose, I'd prefer to have a more readable way to implement it. I need to remember that the validate() function must be declared static.

Feel free to comment with improvements or alternative solutions.

For those courious, the project is open sourced here: https://gitlab.com/ffuga/gutils


Update:

Indeed as @Incomputable has noted, this is a Policy Based design.

Rearranging the code as in the example in the wikipedia page, I fixed the "static" issue.

This is the pretty definitive code:

template<typename Type, typename Validator>
struct ValidatedType : Type, Validator {
    using Validator::validate;
    friend InBitStream &operator>>(InBitStream &stream, ValidatedType<Type, Validator> &v)
    {
        stream >> static_cast<Type &>(v);
        v.validate(v);
        return stream;
    }
};

Usage:

   struct XValidator {
        void validate(XE const &x)
        {
            if (x.underlyingValue() <= 0 || x.underlyingValue() > 4) {
                throw utils::InBitStream::IllegalValueException("Illegal XE value");
            }
        }
    };

Upvotes: 1

Related Questions