简体   繁体   中英

How to link two unrelated classes in C++ free function using template metaprogramming

I have a serialization streaming operator as a free function like this:

struct MyClass { 
    static size_t size() { return 24; } // whatever my expected size is
    X x; Y y; 
};
Archive& operator<<(Archive& ar, MyClass& c) {
    ar << c.x;
    ar << c.y;
    return ar;
}

I have lots of classes and and free function operators like this.

I would like to add a static_assert compile-time check that will fire automatically to do a size check on MyClass (for the case where some developer has added a field to MyClass and forgotten to serialize it). It will call MyClass::size() to get the expected size and compare against sizeof(MyClass) .

I don't want to change all the operator<< definitions to do this. It's tedious, error-prone and it won't implement the original intent: to automatically run the check without having the developer to explicitly write the check (since that'll never happen). Also, the serialization code is from a library, so I don't want to change the original code.

I was thinking that--with metaprogramming--I can let Archive know that I'm serializing MyClass. Then it can do a check like this:

static_assert(sizeof(MyClass) == MyClass::size();

But how to do this? If I make Archive expect a template parameter whose value is MyClass, then every line in the operator<< will have to change since each ar will be an instance of a different class:

Archive<MyClass>& operator<<(Archive<MyClass>& ar, MyClass& c) {
    Archive<X> arX;  arX << c.x;
    Archive<Y> arY;  arY << c.y;
    return ar;
}

Any brilliant ideas? Thank you!

To me, this is only effective if the constant comes from the overload, and you can't snuff the error without updating the overload specifically instead of some class method.

But as you mentioned, you can't add an argument to the << operator. Let's try anyway, since you hinted that you can update the signature, by converting it to a function:

template <size_t N>
using ExpectedSize = std::integral_constant<size_t, N>;

Archive& Serialize(Archive& ar, MyClass& c, ExpectedSize<24>) {
    ar << c.x;
    ar << c.y;
    return ar;
}

Then, invoke that from a catch-all overload:

template <typename T>
Archive& operator <<(Archive ar, T&& c) {
    return Serialize(ar, c, ExpectedSize<sizeof(typename std::remove_reference<T>::type)>{});
}
// non-template overloads for basic primitives
Archive& operator <<(Archive& ar, int c) { return ar; }
Archive& operator <<(Archive& ar, const char* c) { return ar; }

Now, when you run the code and sizeof(MyClass) == 16 instead of 24, you get this error:

error: no matching function for call to 'Serialize(Archive&, MyClass&, ExpectedSize<16>)'
...
note: candidate: 'Archive& Serialize(Archive&, MyClass&, ExpectedSize<24>)'

Demo: https://godbolt.org/z/sBFtBR

If you really wanted a more specific error message, you can add a template to catch the missing overloads:

template <typename T, size_t N>
Archive& Serialize(Archive& ar, T&& c, ExpectedSize<N>) {
    static_assert(sizeof(typename std::remove_reference<T>::type) != N, "Serializer needs to be updated");
}

Then your error message becomes:

<source>: In instantiation of 'Archive& Serialize(Archive&, T&&, ExpectedSize<N>) [with T = MyClass&; long unsigned int N = 16; ExpectedSize<N> = std::integral_constant<long unsigned int, 16>]':
<source>:11:21:   required from 'Archive& operator<<(Archive, T&&) [with T = MyClass&]'
<source>:40:12:   required from here
<source>:34:67: error: static assertion failed: Serializer needs to be updated

Demo: https://godbolt.org/z/wQAvGg

Since your final goal is to enforce consistency between serialization functions and class definitions, you may as well consider automatic generation of serialization methods. This would be more involved than the accepted answer, but may save you some time in long term. I know of two approaches both of which require some nasty tricks: one relies on wrapper classes as descibed here http://cplusplus.bordoon.com/dark_side.html , another is to use xmacro technique https://en.wikipedia.org/wiki/X_Macro to be able to do something like this in the end -> https://github.com/asherikov/ariles#example .

Here's what I came up with. Basically I uses @parktomatomi's idea of a user-written Serialize routine, but now it gets called by a catch-all template<class T> operator<<(Archive&, T& c) that also does the static_assert for the size check.

struct B {
  constexpr static size_t size() { return 20; }
  int y = 200;
};

struct C {
  constexpr static size_t size() { return 10; }
  int x = 100;
  B b;
};

template<typename T>
Archive& Serialize(Archive& ar, T& c) {
  abort();  // Should never get here
}

Archive& operator <<(Archive& ar, int x) {
  std::cout << "ar << " << x << std::endl;
  return ar;
}

template <typename T>
Archive& operator <<(Archive& ar, T& c) {
  static_assert(sizeof(T) == T::size());
  return Serialize<T>(ar, c);
}

template<>
Archive& Serialize(Archive& ar, B& b) {
  std::cout << "ar<B> << " << b.y << std::endl;
  ar << b.y;
  return ar;
}
template<>
Archive& Serialize(Archive& ar, C& c) {
  std::cout << "ar<B> << " << c.x << std::endl;
  ar << c.b;
  ar << c.x;
  return ar;
};

int main(int argc, char* argv[])
{
  Archive ar;
  C c;
  ar << c;

  //std::cout << foo(2);
}

It produces

a.cpp: In instantiation of âArchive& operator<<(Archive&, T&) [with T = B]â:
a.cpp:91:11:   required from here
a.cpp:77:27: error: static assertion failed
   static_assert(sizeof(T) == T::size());
                 ~~~~~~~~~~^~~~~~~~~~~~
a.cpp: In instantiation of âArchive& operator<<(Archive&, T&) [with T = C]â:
a.cpp:100:9:   required from here
a.cpp:77:27: error: static assertion failed

Now I need to come up with a better message.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM