Some might consider this more a proof of failure of the educational system vs. a proof of the primacy of C... 
...and so you've got some competing demands here, if you are talking about "each language"...
It's a super-common pattern in classical imperative programming, where humans are bad at writing forward and reverse transformations and maintaining that code.
I'm not sure you'll get "reversibility" from a specification if the language that interprets it isn't expressive enough in its type system to properly box and unbox everything under the right conditions relative to the spec. We've spoken about this before--about why FP manages to let you express things that aren't intrinsically reversible, yet you can write your program in a way that sort of pretends they are...as if the reverse transformation exists, even though it doesn't. It just sifts the order out so that it never has to actually reverse anything, but figures the meaningful ordering out (in terms of things like bind
and return
, if monads, but not everything is a monad).
(Take the above imprecise language with a grain of salt.)
What you need are combinators, and to get those combinators you need to lean on a type system. Or failing that, something like Rebol Ren-C that is so paradigm neutral that you wind up having to tailor it ad-hoc to where someone might ask you why you are reinventing the wheel. I'm just trying to make it unusually easy for laypeople who want to play with a Minecraft-like-programming model to do these things.
("We must not forget that the wheel is reinvented so often because it is a very good idea; I've learned to worry more about the soundness of ideas that were invented only once." --David Parnas)
@bradrn would know more than me about the specifics if you want to draw inspiration from Haskell. But if you're looking for the "right" answer here you need something "combinator-like". Maybe cereal or Data.Binary.Parser
Since my Haskell is weak I asked an AI to give an example, e.g. for ZIP, here's a bit of a suggestion:
data ZipEntry = ZipEntry {
signature :: Word32,
compression :: Word16,
filename :: String,
data :: ByteString
} deriving (Show, Eq)
zipEntryFormat :: BinaryFormat ZipEntry
zipEntryFormat = do
sig <- field "signature" (constant 0x04034b50)
comp <- field "compression" word16LE
fname <- field "filename" (lengthPrefixed word16LE utf8String)
dat <- field "data" (remainingBytes)
pure $ ZipEntry sig comp fname dat
Core mechanism something like this:
data BinaryFormat a = BinaryFormat {
parseFrom :: ByteString -> Either Error (a, ByteString),
serialize :: a -> ByteString
}
You can ask your nearest AI to explain how it works. But here's some fake C++:
// This is NOT real C++, but illustrates the concept
template<typename T>
class BinaryFormat {
public:
virtual T parse(const std::vector<uint8_t>& bytes) = 0;
virtual std::vector<uint8_t> serialize(const T& value) = 0;
};
// Each primitive must implement both directions
class Word16Format : public BinaryFormat<uint16_t> {
public:
uint16_t parse(const std::vector<uint8_t>& bytes) override {
return bytes[0] | (bytes[1] << 8);
}
std::vector<uint8_t> serialize(const uint16_t& value) override {
return {uint8_t(value), uint8_t(value >> 8)};
}
};
// Composing formats preserves bidirectionality
template<typename T1, typename T2>
class PairFormat : public BinaryFormat<std::pair<T1, T2>> {
BinaryFormat<T1>* first;
BinaryFormat<T2>* second;
// ... implementation that uses both parse and serialize from sub-formats
};
So far, what we've done is more like Kaitai than it is like combinators:
But, as UPARSE has shown, Ren-C can tackle higher-order, combinator-like problems.
If you want an introduction to combinator-based thinking, this article is pretty good. 
Parsing Huge Simulated Streams In Attoparsec | AE1020: Lazy Notebook