Forced and easy hton/ntoh conversion
Unqualified names from C headers

It appears that your &lt;cstdint&gt; happens to declare its identifiers in the global namespace as well as in std; this means that you weren't notified of the consistent misspelling of std::uint8_t, std::uint16_t and the like.

Interface

It's not clear why ExtractValue is visible at file scope (rather than being a private static method within NetworkData).

The get() methods ought to be declared const (and if you never implement set() methods, then perhaps _p should be a pointer-to-const).

Pass pointer to member rather than offsetof

The use of offsetof() and pointer arithmetic with std::uint8_t* (which should be char*, given that offsetof() works in units of char) is deeply disturbing.  One possibility is for NetworkData&lt;T&gt; to hold a private reference to the T, and accept pointer-to-member to indicate which value is desired:

#include &lt;type_traits&gt;
#include &lt;utility&gt;

// Get a field of the class T, in host byte order.
// The class T is wrapped in NetworkData class. The get() function
// takes the offset to the member as an argument, gets the address
// of field and extract the field
template &lt;typename T&gt;
class NetworkData
{
    const T&amp; data;

public:
    NetworkData (void *data)
        : data(*reinterpret_cast&lt;T*&gt;(data))
    {
    }

    template&lt;typename V&gt;
    V get(V T::*field) const
    {
#if BIG_ENDIAN
        return data.*field;
#else
        return byte_swap(data.*field);
#endif
    }

private:
    template&lt;typename V&gt;
    static
    std::enable_if_t&lt;std::is_integral_v&lt;V&gt; &amp;&amp; std::is_signed_v&lt;V&gt;, V&gt;
    byte_swap(V value)
    {
        // We manipulate using unsigned bitwise arithmetic.
        return reinterpret_cast&lt;V&gt;(byte_swap(reinterpret_cast&lt;std::make_unsigned_t&lt;V&gt;&gt;(value)));
    }

    template&lt;typename V&gt;
    static
    std::enable_if_t&lt;std::is_integral_v&lt;V&gt; &amp;&amp; std::is_unsigned_v&lt;V&gt;, V&gt;
    byte_swap(V value)
    {
        // We assume CHAR_BIT == 8 (so that sizeof gives us octets)
        static const V mask = 0xff;
        V result = 0;
        for (auto shift = 0u;  shift &lt; 8 * sizeof value;  shift += 8)
            result = (result &lt;&lt; 8) + ((value &gt;&gt; shift) &amp; mask);
        return result;
    }

    // We could add overloads here to take advantage of processor-native byte swap instructions, as available.
};


We can then use it like this:

#include &lt;cstdint&gt;
struct Msg {
   std::uint32_t a;
   std::uint16_t b;
};

void process (std::uint8_t *p)
{
    NetworkData&lt;Msg&gt; h(p);

    auto a = h.get(&amp;Msg::a);
    auto b = h.get(&amp;Msg::b);
}


This eliminates the use of preprocessor macros, and makes a slightly more natural object-access syntax.

Consider modifying the structure members

If we're allowed to re-write Msg, we could make its members be network-endian values:

#include &lt;cstdint&gt;
struct Msg {
   NetworkEndian&lt;std::uint32_t&gt; a;
   NetworkEndian&lt;std::uint16_t&gt; b;
};


We can declare NetworkEndian like this:

template&lt;typename Integer&gt;
class NetworkEndian
{
    Integer value;

public:
    NetworkEndian(Integer value)
        : value(byte_swap(value))
    {}
    // default copy, assign, destructor

    operator Integer() const
    {
        return byte_swap(value);
    }

private:
    static Integer byte_swap(Integer v)
    {
        using Unsigned = std::make_unsigned_t&lt;Integer&gt;;
        Unsigned u = reinterpret_cast&lt;Unsigned&gt;(v);
        // Assume CHAR_BIT == 8 (so that sizeof gives us octets)
        static const Unsigned mask = 0xff;
        Unsigned result = 0;
        for (auto shift = 0u;  shift &lt; 8 * sizeof u;  shift += 8)
            result = (result &lt;&lt; 8) + ((u &gt;&gt; shift) &amp; mask);
        return reinterpret_cast&lt;Integer&gt;(result);
    }
};


The conversion operators mean that we can now read the members in a completely natural manner:

void process(const Msg&amp; message)
{
    auto a = message.a;
    auto b = message.b;
}


Not only that, but by providing a converting constructor, we also get to assign values equally naturally.