cbeck88/visit_struct

Recursive Serialization, decltype and is_visitable behaving strangely

churchianity opened this issue · 7 comments

I'm trying to write a function which, given some passed-in visitable struct, will recursively serialize all of its members that are marked visitable (I use the instrusive method).

template<typename T>
char* visitSerialize(const T& type) {
    visit_struct::for_each(type, [](const char* name, const auto & value) {
        if (visit_struct::traits::is_visitable<decltype(value)>::value) {
            println("visitable! %s", name);
        }
    });
    return ""; // @TODO
}

my plan was to recursively call visitSerialize on members of the currently-being-visited struct, if the member I am currently inspecting is visitable. However, value in the arguments to my callback is never considered visitable by the expression visit_struct::traits::is_visitable<decltype(value)>::value, despite them being marked as such in my header file(s). Here's a simple example:

I start by calling visitSerialize on an instance of this:

typedef struct Level {
    BEGIN_VISITABLES(Level);
    VISITABLE(char*, name);

    VISITABLE(Terrain*, terrain);
    VISITABLE(WaterRect*, waterRects);
    VISITABLE(Entity*, entities);
    VISITABLE(Skybox*, skybox);
    VISITABLE(PointLight*, pointLights);
    VISITABLE(DirLight*, dirLights);
    VISITABLE(SpotLight*, spotLights);

    VISITABLE(u32, numWaterRects);
    VISITABLE(u32, numEntities);
    VISITABLE(u32, numPointLights);
    VISITABLE(u32, numDirLights);
    VISITABLE(u32, numSpotLights);

    u32 terrainShaderId;
    u32 entityShaderId;
    END_VISITABLES;
} Level;

None of the struct types there are considered visitable. Here's water.h:

typedef struct WaterRect {
    BEGIN_VISITABLES(WaterRect);
    VISITABLE(glm::vec3, origin);
    VISITABLE(float, xwidth);
    VISITABLE(float, zwidth);

    VISITABLE(float, moveSpeed);
    VISITABLE(float, moveFactor);
    VISITABLE(float, waveStrength);

    u32 refractionFramebuffer;
    u32 refractionTextureId;
    u32 refractionDepthTextureId;

    u32 reflectionFramebuffer;
    u32 reflectionTextureId;

    VISITABLE(glm::vec4, color);
    END_VISITABLES;
} WaterRect;

Am I missing something? Is there a better way? Does decltype not work the way I thought?

I'm not a C++ template guy so a lot of this stuff is black magic to me. Thanks.

I'm using clang v13.0.0 on macosx catalina, c++14

I must be doing something fundamentally wrong, because I just tested using the VISITABLE_STRUCT method instead of the instrusive method and everything is still reading as 'not visitable'.

The issue might be that decltype(value) gives you a const reference type. Maybe try visit_struct::traits::is_visitable<std::decay_t<decltype(value)>>::value. "decay" should remove the const reference and give you the underlying type.

Thanks for the reply. No dice it seems. Minimally reproducible example:

#include <stdio.h>
#include <stdlib.h>

#include <visit_struct/visit_struct.hpp>
#include <visit_struct/visit_struct_instrusive.hpp>

typedef struct Water {
    BEGIN_VISITABLES(Water);
    VISITABLE(float, x);
    VISITABLE(float, y);
    VISITABLE(float, z);
    END_VISITABLES;
} Water;

typedef struct Level {
    BEGIN_VISITABLES(Level);
    VISITABLE(Water*, waterTiles);
    VISITABLE(int, numWaterTiles);
    END_VISITABLES;
} Level;

template<typename T>
static void visitSerialize(const T& type) {
    visit_struct::for_each(type, [](const char* name, const auto & value) {
        if (visit_struct::traits::is_visitable<std::decay_t<decltype(value)>>::value) {
            printf("is visitable! %s", name);
        }
    });
}

int main(void) {
    Level level;
    level.waterTiles = (Water*) malloc(sizeof(Water) * 12);
    level.numWaterTiles = 12;

    visitSerialize(level);

    return 0;
}

clang main.cpp -std=c++14 -I . -o out (assuming you have visit struct in that dir)

I tried using std::decay_t as well as just decltype as I did before.

I recognize this isn't exactly an issue with the library itself, but I was assuming there would be a way to visit recursively on structs. Was I mistaken, or is there a better way to do it?

Inspecting some code you have written @NikolausDemmel in one of your public repositories, it certainly seems like it is possible. Looking at this example:

// convert visitable struct to json; supports types that can be assigned to
// nlohmann::json, such as strings, doubles, as well as recursive std::vector
// and visistable structs;
template <class Value>
auto visitable_to_json(const Value& value) {
  nlohmann::json result;
  if constexpr (visit_struct::traits::is_visitable<Value>::value) {
    // visitable struct --> visit each member and save as json object
    visit_struct::for_each(value, [&](const char* name, const auto& member) {
      result[name] = visitable_to_json(member);
    });
  } else if constexpr (is_vector_v<Value>) {
    // vector --> recursively save elements to json array
    for (const auto& elem : value) {
      result.push_back(visitable_to_json(elem));
    }
  } else if constexpr (wise_enum::is_wise_enum_v<Value>) {
    // enum --> save as string
    result = wise_enum::to_string(value);
  } else {
    // other basic value (string, number, ...)
    result = value;
  }
  return result;
}

Trying to work through comparing your assumedly working code and mine.

Just noticed that your member is a pointer to an array of Water. The pointer is of course not visitable. You need to dereference and visit the array elements.

Thanks. I managed to get it working by essentially copying up to the first if-block in your JSON serialization example, AND making sure that I always pass my structs to visitSerialize by value.

If I call visitSerialize(Level* level) it doesn't work, but if I call visitSerialize(Level level), it does.

Makes sense to me in retrospect. I'll close the issue, appreciate the help. Ideally I'd like to be able to pass the struct in by pointer too, but I can probably figure that out.