Search code examples
c++boostc++17boost-hana

Not a constant expression in lambda function


I have the following code:

#include <boost/hana.hpp>
#include <array>
#include <iostream>
#include <utility>

namespace hana = boost::hana;

#define HEADER_CONNECT 0b00010000
#define HEADER_CONNACK 0b00001000

struct ConnectFrame
{
    uint8_t header = 16;
    uint8_t variable = 2;
};

struct ConnackFrame
{
    uint8_t header = 8;
    uint8_t variable = 3;
};

constexpr auto FramesMap = hana::make_tuple(
    hana::make_pair(hana::type_c<ConnectFrame>, hana::integral_c<std::uint8_t, HEADER_CONNECT>),
    hana::make_pair(hana::type_c<ConnackFrame>, hana::integral_c<std::uint8_t, HEADER_CONNACK>));

//! Runtime deserialization switch based on FramesMap
template <typename InputIterator>
auto deserializeByFrameHeader(const std::uint8_t frameHeader, const InputIterator buffer)
{
    auto found = hana::index_if(FramesMap, [&frameHeader = std::as_const(frameHeader)](auto const &pair) {
        return hana::second(pair) == hana::integral_c<std::uint8_t, frameHeader>;
    });
    auto FrameType = hana::first(hana::at(FramesMap, found.value()));
    using T = typename decltype(FrameType)::type;
    T var;
    //deserialize(buffer, var);
    return var;
}

int main()
{
    std::array<std::byte, 128> buffer;
    // for dummy purposes we assume that the first byte of the buffer array after serialization is 8
    const uint8_t header = 8;
    ConnackFrame frameOut = deserializeByFrameHeader(header, buffer.begin());
}

Live demo

I try to find the index of the pair in the tuple which matches the variable frameHeader. Unfortunately, I get a compilation error:

../include/minimalMQTT.hpp:178:43: error: 'this' is not a constant expression
  178 |                 return hana::second(pair) == hana::integral_c<std::uint8_t, frameHeader>;
      |                        ~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

How can the variable frameHeader be declared as a constant expression in order to make this work?


Solution

  • hana::integral_c<std::uint8_t, frameHeader>
    

    integral_c is a type that encodes a statically known value. However you try to instantiate it with frameHeader which is not statically known.

    To map runtime-values on compile-tume values the best you can do is a mapping (sometimes using binary search). However, you can also check whether you require this compiletime evaluated.

    Workaround

    In your specific example you can work around things by using a constexpr lambda (given a recent enough compiler!).

    In practice I doubt this would suit your needs, ever, but just so you're aware of the trick:

    Live On Wandbox

    #include <boost/hana.hpp>
    #include <boost/core/ignore_unused.hpp>
    #include <array>
    #include <iostream>
    #include <utility>
    
    namespace hana = boost::hana;
    
    #define HEADER_CONNECT 0b00010000
    #define HEADER_CONNACK 0b00001000
    
    struct ConnectFrame
    {
        uint8_t header = 16;
        uint8_t variable = 2;
    };
    
    struct ConnackFrame
    {
        uint8_t header = 8;
        uint8_t variable = 3;
    };
    
    constexpr auto FramesMap = hana::make_tuple(
        hana::make_pair(hana::type_c<ConnectFrame>, hana::integral_c<std::uint8_t, HEADER_CONNECT>),
        hana::make_pair(hana::type_c<ConnackFrame>, hana::integral_c<std::uint8_t, HEADER_CONNACK>));
    
    //! Runtime deserialization switch based on FramesMap
    template <typename FrameHeader, typename InputIterator>
    auto deserializeByFrameHeader(FrameHeader const frameHeader, const InputIterator buffer)
    {
        auto found = hana::index_if(FramesMap, [=](auto const &pair) constexpr {
            return hana::second(pair) == hana::integral_c<std::uint8_t, frameHeader()>;
        });
        auto FrameType = hana::first(hana::at(FramesMap, found.value()));
        using T = typename decltype(FrameType)::type;
        T var;
        boost::ignore_unused(buffer);
        //deserialize(buffer, var);
        return var;
    }
    
    int main()
    {
        std::array<std::byte, 128> buffer;
        // for dummy purposes we assume that the first byte of the buffer array after serialization is 8
        ConnackFrame frameOut = deserializeByFrameHeader(
                []() constexpr { return 8; },
                buffer.begin());
    
        boost::ignore_unused(frameOut);
    }
    

    UPDATES

    Technically, the header is the first byte of the buffer array, i.e. uint8_t header = (uint8_t)buffer[0]. Would it be possible to omit the header argument and extract the header from the buffer as a constexpr directly?

    No.

    The return type is fixed. The input is dynamic. There's no way to pave over that (usefully/efficiently).

    By the way, what would be a solution if I don't need it compile time evaluated?

    Since you're parsing protocol messages you're naturally switching on type ids (because that's how they exist on the wire). As a serious C++ programmer you naturally want to jump the abstraction layer to proper type-switching as soon as possible.

    • The old-fashioned technique would be dynamic polymorphism (virtual interfaces and inheritance)
    • Modern day mechanisms include std::variant<...> with visitation.

    Depending on your usage patterns and processing needs either might be more applicable. std::variant has the nice feature that it encodes the type in a switchable manner, but visitation retains static type information. This means: technically you may be able to leverage static type information, inlining and all the optimization goodness.

    It seems that this is what you're after. So I'd suggest:

    Live On Coliru

    #include <array>
    #include <iostream>
    #include <variant>
    
    constexpr uint8_t HEADER_CONNECT = 0b00010000;
    constexpr uint8_t HEADER_CONNACK = 0b00001000;
    
    struct ConnectFrame {
        uint8_t header = 16;
        uint8_t variable = 2;
    };
    
    struct ConnackFrame {
        uint8_t header = 8;
        uint8_t variable = 3;
    };
    
    // Static typed land
    void handler(ConnectFrame const&) { std::cout << "Handling ConnectFrame\n"; }
    void handler(ConnackFrame const&) { std::cout << "Handling ConnackFrame\n"; }
    
    template <typename InputIterator>
    void deserialize(InputIterator&, ConnectFrame&) { /*TODO*/ }
    
    template <typename InputIterator>
    void deserialize(InputIterator&, ConnackFrame&) { /*TODO*/ }
    
    template <typename Frame, typename InputIterator>
    Frame deserialize(InputIterator& buffer) {
        Frame frame;
        deserialize(buffer, frame);
        return frame;
    }
    
    // Type-swithcing land
    template <typename InputIterator>
    constexpr inline std::uint8_t frameHeader(InputIterator& buffer) {
        return static_cast<std::uint8_t>(*buffer++);
    }
    
    using AnyFrame = std::variant<ConnectFrame, ConnackFrame>;
    
    template <typename InputIterator>
    AnyFrame deserializeByFrameHeader(InputIterator&& buffer) {
        switch (uint8_t h = frameHeader(buffer)) {
            case HEADER_CONNECT: return deserialize<ConnectFrame>(buffer);
            case HEADER_CONNACK: return deserialize<ConnackFrame>(buffer);
        }
        throw std::range_error("frameHeader");
    }
    
    int main() {
        constexpr auto process = [](auto const& frame) { handler(frame); };
        using Buffer = std::array<std::byte, 128>;
    
        for (auto buffer : { Buffer 
            { std::byte(HEADER_CONNECT), std::byte(0x12), std::byte(0x34), },
            { std::byte(HEADER_CONNACK), std::byte(0xab), std::byte(0xcd), } })
        {
            auto frameOut = deserializeByFrameHeader(buffer.begin());
            std::visit(process, frameOut);
        }
    }
    

    Which prints

    Handling ConnectFrame
    Handling ConnackFrame
    

    Using the Hana Mapping

    If you really think it's important to work from the mappings table, you can, using a bit more code and compiler sweat:

    constexpr auto FramesMap = hana::make_tuple(
        hana::make_pair(hana::type_c<ConnectFrame>, HEADER_CONNECT),
        hana::make_pair(hana::type_c<ConnackFrame>, HEADER_CONNACK)
    );
    

    Note how I dropped the integral_c because we don't need it

    Let's make AnyFrame a variant over the frame-types:

    constexpr auto FrameTypes = hana::transform(FramesMap, hana::first);
    
    using AnyFrame = decltype(
            hana::unpack(FrameTypes, hana::template_<std::variant>))
        ::type;
    

    Now, let's reimplement deserializeByFrameHeader using it:

    template <typename InputIterator>
    AnyFrame deserializeByFrameHeader(InputIterator&& buffer) {
        AnyFrame retval;
    
        hana::for_each(FramesMap,
            [&, frameHeader = frameHeader(buffer)](auto const &pair) {
                auto first = hana::first(pair);
                using T = typename decltype(first)::type;
    
                if (hana::second(pair) == frameHeader) {
                    retval.emplace<T>();
                    deserialize(buffer, std::get<T>(retval));
                }
            });
    
        return retval;
    }
    

    Note the the simplification: we kept everything depending on the static type of the tuple element (pair) inside the polymorphic lambda, where we have the frametype available at all times.

    Full Demo

    Live On Coliru

    #include <cstdint>
    constexpr uint8_t HEADER_CONNECT = 0b00010000;
    constexpr uint8_t HEADER_CONNACK = 0b00001000;
    
    struct ConnectFrame {
        uint8_t header = 16;
        uint8_t variable = 2;
    };
    
    struct ConnackFrame {
        uint8_t header = 8;
        uint8_t variable = 3;
    };
    
    #include <boost/hana.hpp>
    #include <stdexcept>
    #include <variant>
    #include <iostream>
    
    namespace {
        namespace hana = boost::hana;
    
        constexpr auto FramesMap = hana::make_tuple(
            hana::make_pair(hana::type_c<ConnectFrame>, HEADER_CONNECT),
            hana::make_pair(hana::type_c<ConnackFrame>, HEADER_CONNACK)
        );
    
        constexpr auto FrameTypes = hana::transform(FramesMap, hana::first);
    
        using AnyFrame = decltype(
                hana::unpack(FrameTypes, hana::template_<std::variant>))
            ::type;
    }
    
    // Static typed land
    void handler(ConnectFrame const&) { std::cout << "Handling ConnectFrame\n"; }
    void handler(ConnackFrame const&) { std::cout << "Handling ConnackFrame\n"; }
    
    template <typename InputIterator>
    void deserialize(InputIterator&, ConnectFrame&) { /*TODO*/ }
    
    template <typename InputIterator>
    void deserialize(InputIterator&, ConnackFrame&) { /*TODO*/ }
    
    // Type-swithcing land
    template <typename InputIterator>
    constexpr inline std::uint8_t frameHeader(InputIterator& buffer) {
        return static_cast<std::uint8_t>(*buffer++);
    }
    
    template <typename InputIterator>
    AnyFrame deserializeByFrameHeader(InputIterator&& buffer) {
        AnyFrame retval;
    
        hana::for_each(FramesMap,
            [&, frameHeader = frameHeader(buffer)](auto const &pair) {
                auto first = hana::first(pair);
                using T = typename decltype(first)::type;
    
                if (hana::second(pair) == frameHeader) {
                    retval.emplace<T>();
                    deserialize(buffer, std::get<T>(retval));
                }
            });
    
        return retval;
    }
    
    #include <array>
    int main() {
        constexpr auto process = [](auto const& frame) { handler(frame); };
        using Buffer = std::array<std::byte, 128>;
    
        for (auto buffer : { Buffer 
            { std::byte(HEADER_CONNECT), std::byte(0x12), std::byte(0x34), },
            { std::byte(HEADER_CONNACK), std::byte(0xab), std::byte(0xcd), } })
        {
            auto frameOut = deserializeByFrameHeader(buffer.begin());
            std::visit(process, frameOut);
        }
    }
    

    Prints

    Handling ConnectFrame
    Handling ConnackFrame