1

My purpose is getting any value from the data array without specifying the type every time when I'm getting the value. I created special tables that describes the field information (field name and type) and also wrote a function that helps me to interpret data properly.

The code is presented below:

#include <iostream>
#include <variant>
#include <assert.h>
#include <string_view>
#include <unordered_map>

enum class ValueType : uint8_t
{
    Undefined       = 0x00,
    Uint32,
    AsciiString,
};

typedef uint64_t FieldId;

struct FieldInfo
{
    std::string _name;
    ValueType _type;
};

typedef std::unordered_map<FieldId, FieldInfo> FieldContainer;

static FieldContainer requestFields =
{
    { 0, { "user-id",   ValueType::Uint32, } },
    { 1, { "group-id",  ValueType::Uint32, } },
};

std::variant<uint8_t, uint32_t, std::string_view> getValue(ValueType type,
                                                           const uint8_t* data,
                                                           size_t length)
{
    if (type == ValueType::Uint32)
    {
        assert(length == sizeof(uint32_t));
        return *reinterpret_cast<const uint32_t*>(data);
    }
    else if (type == ValueType::AsciiString)
    {
        return std::string_view(reinterpret_cast<const char*>(data), length);
    }

    return static_cast<uint8_t>(0);
}


int main(int argc, char *argv[])
{
    const uint8_t arr[] = {0x00, 0x11, 0x22, 0x33};
    size_t length = sizeof(arr);

    const auto value = getValue(ValueType::Uint32, arr, length);

    std::visit([](auto&& arg)
                    {
                        if ( arg == 0x33221100 )
                        {
                            std::cout << "Value has been found" << std::endl;
                        }
                    }, value);
    return 0;
}

I expect that the compiler would deduce the return value properly and let me do the numbers comparing. However, I received the following compiler messages:

error: no match for ‘operator==’ (operand types are ‘const std::basic_string_view<char>’ and ‘int’)
   57 |                         if ( arg == 0x33221100 )
      |                              ~~~~^~~~~~~~~~~~~

error: invalid conversion from ‘int’ to ‘const char*’ [-fpermissive]
   57 |                         if ( arg == 0x33221100 )
      |                                     ^~~~~~~~~~
      |                                     |
      |                                     int

I know that I can get value via calling std::get:

    if ( std::get<uint32_t>(value) == 0x33221100 )
    {
        std::cout << "Value has been found" << std::endl;;
    }

But it is not what I want to achieve.

The question is - can I use the presented approach to get a value without specifying the type in each place of code where I need that?

Environment info:

  • OS: Linux
  • Compiler: g++ (GCC) 11.1.0
  • Standard: C++17
1201ProgramAlarm
  • 32,384
  • 7
  • 42
  • 56
slinkin
  • 375
  • 3
  • 15
  • 1
    `arg` could be `uint8_t`, `uint32_t` or `std::string_view`. The body of the lambda must be valid for all three. Whereas `arg == 0x33221100` doesn't make sense when `arg` is of type `std::string_view`. – Igor Tandetnik Jul 10 '21 at 14:25
  • How can the compiler know what variant contains at *run-time*?! – Igor R. Jul 10 '21 at 14:27
  • Guys, I understand the problem. I expect that compiler detects that previously I called `getValue` for Unit32 and therefore I can legally do number comparing. Do I understand correctly that `std::visit` always work at runtime? – slinkin Jul 10 '21 at 14:31
  • 1
    You seem to expect the compiler to look inside `getValue`, perform full semantic analysis of its business logic, and deduce that it would produce a variant holding `uint32_t` when called with `ValueType::Uint32`. If so, you expect too much. – Igor Tandetnik Jul 10 '21 at 14:35
  • 1
    If you always pass a hard-coded constant as a first parameter of `getValue`, then you'd be better off just writing three separate functions like `getInt8Value`, `getInt32Value` and `getStringValue`, with proper return types, and don't bother with `variant` at all. In fact, you can write those functions as well as `getValue` that would simply call them. – Igor Tandetnik Jul 10 '21 at 14:38
  • Exactly. There is simple if conditions that get to understand what type will be returned. Moreover, I pass `Uint32` directly to the function that gives a guarantee the function returns `unit32_t`. – slinkin Jul 10 '21 at 14:39
  • 1
    Well, that ain't how C++ works. This is wishful thinking. – Igor Tandetnik Jul 10 '21 at 14:40
  • What you can do is make a custom 'functor' class with `operator ()` overloaded for the various types of the variant, and then use an instance of that class instead of the lambda. – MicroVirus Jul 10 '21 at 14:49

2 Answers2

2

One fundamental rule of C++ when it comes to optimizations of any kind is that no compiler optimization can have any "observable effects".

This means, amongst other things, that well-formed code cannot be optimized into ill-formed code. And ill-formed code cannot be optimized into well-formed code.

const auto value = getValue(ValueType::Uint32, arr, length);

getValue() is declared as returning a std::variant. That's what it's return type is, and this is what value's type gets deduced to. value is the declared std::variant. Full stop. Whether it can or cannot actually have any particular value is immaterial for the purpose of determining whether the code is well-formed or not. The subsequent std::visit, therefore, must be well-formed for all possible variant values, and you already understand that it's not for one of them.

Well, that's pretty much the end of the story. The shown code is ill-formed and not valid C++. It is true that in the shown code getValue() does not return a variant holding a value for which the subsequent std::visit is ill-formed. However that's immaterial for the reasons stated.

All of the above also means the following: if the declared variant either cannot hold a value for which the std::visit is ill-formed, or the visitor's code is adjusted so it's well-formed, then: if the compiler can deduce the actual type that's returned here, the compiler may (but it's no required to) completely optimize away and not construct the variant in the first place, and just generate the code that returns the only possible value/values, and visits each value directly. This is because eliminating the construction/destruction of the variant will have no observable effects.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
1

You can use "if constexpr" so you can get around the problem Igor Tandetnik mentioned. If you can use c++20 you could use concepts combined with "if constexpr" to group types and handle them so you do not have to write an if for every type.

Example:

#include <cstdint>
#include <iostream>
#include <type_traits>
#include <variant>
struct StructA
{
};

struct StructWithCoolFunction
{
  void
  coolFunction ()
  {
  }
};

struct AnotherStructWithCoolFunction
{
  void
  coolFunction ()
  {
  }
};

struct UnhandledStruct
{
};

typedef std::variant<StructA, StructWithCoolFunction, AnotherStructWithCoolFunction, UnhandledStruct> MyVariant;
template <typename T> concept HasCoolFunction = requires(T t) { t.coolFunction (); };

auto const handleVariant = [] (auto &&arg) {
  using ArgType = typename std::remove_cv<typename std::remove_reference<decltype (arg)>::type>::type;
  if constexpr (std::is_same<StructA, ArgType>::value)
    {
      std::cout << "ArgType == StructA" << std::endl;
    }
  else if constexpr (HasCoolFunction<ArgType>)
    {
      std::cout << "some struct with a function 'coolFunction ()'" << std::endl;
    }
  else
    {
      std::cout << "not handled type" << std::endl;
    }
};

int
main ()
{
  auto variantWithStructA = MyVariant{ StructA{} };
  std::visit (handleVariant, variantWithStructA);
  auto variantWithStructWithCoolFunction = MyVariant{ StructWithCoolFunction{} };
  std::visit (handleVariant, variantWithStructWithCoolFunction);
  std::visit (handleVariant, MyVariant{ UnhandledStruct{} });
  return 0;
}

If you do not have c++20 you can also try to find a solution with type_traits pre c++20 maybe:

if constexpr(std::is_integral<ArgType>::value) 

could help you.

Koronis Neilos
  • 700
  • 2
  • 6
  • 20
  • Koronis, thanks a lot for your answer! I'm able to use C++20. It is very likely what I needed. I compiled that and disassembled the binary. I was impressed that the compiler detects all types and generated only 3 std::cout calling. I will try to apply that solution for my code. – slinkin Jul 12 '21 at 15:54
  • please mark it as answer if it answers your question. – Koronis Neilos Jul 12 '21 at 18:42