clay icon indicating copy to clipboard operation
clay copied to clipboard

Tip: C++20 and `-Wall -Werror`

Open linkdd opened this issue 8 months ago • 3 comments

Context

Designated field initializers were introduced in C++20. They require that all of them are specified, and in the correct order:

struct foo { int a; int b };

foo x = { .a = 1 }; // WARNING: missing field initializer

Clay's implementation declare a few global variables with missing field initializers, such as:

// missing "isStaticallyAllocated"
Clay_String CLAY__SPACECHAR = { .length = 1, .chars = " " };

And the CLAY macros expands the argument as an initializer for the Clay_ElementDeclaration structure, this would require to initialize all of its fields every single time. Which is quite verbose.

If you omit those, and compile with -Wall -Werror, you'll get a lot of errors because of the missing field initializers.

Prelude: Compile Clay implementation as a C file

In a clay.c, NOT clay.cpp, file:

#define CLAY_IMPLEMENTATION
#include <clay.h>

And then in your build system, for example CMake:

file(GLOB_RECURSE MY_SOURCES
  ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp
  ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c
)

Solution 1: Turn off the warning (not recommended)

Compiling with -Wno-missing-field-initializers would simply disable the message.

I am not a fan of this solution because it turns off such warning for your whole codebase, and I rely on this to catch potential errors related to uninitialized fields.

Solution 2: Push a pragma

We can disable the warning locally:

#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-field-initializers"
#elif defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#else
#warning "Unknown compiler, missing-field-initializers may not be ignored"
#endif

// your Clay layout

#if defined(__clang__)
#pragma clang diagnostic pop
#elif defined(__GNUC__)
#pragma GCC diagnostic pop
#endif

Once again, not a fan of this solution, because code within the layout might have errors due to field that were not initialized, and the compiler cannot catch it anymore.

Solution 3: A macro and a lambda

We define this macro:

#define CLAY_ELEMENT_CONFIG(...)          \
  ([&]() {                                \
    auto cfg = Clay_ElementDeclaration{}; \
    __VA_ARGS__                           \
    return cfg;                           \
  }())

This creates and call immediately a lambda. The element declaration is default initialized, and then we inject a sequence of statements to initialize the config. Use like this:

CLAY(CLAY_ELEMENT_CONFIG(
  cfg.id                   = CLAY_ID("OuterContainer");
  cfg.layout.sizing.width  = CLAY_SIZING_GROW(0, 0); // both arguments required to avoid missing field initializers
  cfg.layout.sizing.height = CLAY_SIZING_GROW(0, 0);
  cfg.layout.padding       = CLAY_PADDING_ALL(16);
  cfg.layout.childGap      = 16;
)) {
  CLAY(CLAY_ELEMENT_CONFIG(
    cfg.id                   = CLAY_ID("SideBar");
    cfg.layout.sizing.width  = CLAY_SIZING_FIXED(200);
    cfg.layout.sizing.height = CLAY_SIZING_GROW(0, 0);
    cfg.layout.padding       = CLAY_PADDING_ALL(16);
    cfg.layout.childGap      = 16;
    cfg.backgroundColor      = {224, 215, 210, 255};
  )) {
    CLAY(CLAY_ELEMENT_CONFIG(
      cfg.id                   = CLAY_ID("Item 1");
      cfg.layout.sizing.width  = CLAY_SIZING_GROW(0, 0);
      cfg.layout.sizing.height = CLAY_SIZING_FIXED(32);
      cfg.backgroundColor      = {255, 0, 0, 255};
    )) {}
    CLAY(CLAY_ELEMENT_CONFIG(
      cfg.id                   = CLAY_ID("Item 2");
      cfg.layout.sizing.width  = CLAY_SIZING_GROW(0, 0);
      cfg.layout.sizing.height = CLAY_SIZING_FIXED(32);
      cfg.backgroundColor      = {0, 255, 0, 255};
    )) {}
    CLAY(CLAY_ELEMENT_CONFIG(
      cfg.id                   = CLAY_ID("Item 3");
      cfg.layout.sizing.width  = CLAY_SIZING_GROW(0, 0);
      cfg.layout.sizing.height = CLAY_SIZING_FIXED(32);
      cfg.backgroundColor      = {0, 0, 255, 255};
    )) {}
  }
}

Unfortunately, I think it adds an overhead because of the lambda. I am not sure the compiler is able to inline it. But this is the cleanest looking solution I could find that allow me to not disable the warning.


Feel free to comment and propose something else.

linkdd avatar Jul 04 '25 11:07 linkdd

Solution 4: Not using the CLAY macro actually...

Clay__OpenElement();

auto cfg = Clay_ElementDeclaration{};
cfg.id                   = CLAY_ID("OuterContainer");
cfg.layout.sizing.width  = CLAY_SIZING_GROW(0, 0); // both arguments required to avoid missing field initializers
cfg.layout.sizing.height = CLAY_SIZING_GROW(0, 0);
cfg.layout.padding       = CLAY_PADDING_ALL(16);
cfg.layout.childGap      = 16;

Clay__ConfigureOpenElement(cfg);

// ...

Clay__CloseElement();

I can wrap the "open" part in begin_* functions and the "close" part in end_* functions, and design properly their arguments so that I have an API that matches more ImGui for example.

In the end, this allows me to separate the "styling" from the UI logic itself, for example:

void app::on_gui(entt::registry& registry, bool prerender) {
  auto lb = engine::ui::layout_builder{registry, prerender, CLAY_ID("MainLayout")};

  auto root_style = decltype(engine::ui::layout_builder::element_config::style){};
  root_style.layout.sizing.width  = CLAY_SIZING_GROW(0, 0);
  root_style.layout.sizing.height = CLAY_SIZING_GROW(0, 0);
  root_style.layout.padding       = CLAY_PADDING_ALL(16);
  root_style.layout.childGap      = 16;

  auto sidebar_style = decltype(engine::ui::layout_builder::element_config::style){};
  sidebar_style.layout.sizing.width    = CLAY_SIZING_FIXED(200);
  sidebar_style.layout.sizing.height   = CLAY_SIZING_GROW(0, 0);
  sidebar_style.layout.padding         = CLAY_PADDING_ALL(16);
  sidebar_style.layout.childGap        = 16;
  sidebar_style.layout.layoutDirection = CLAY_TOP_TO_BOTTOM;
  sidebar_style.backgroundColor        = {64, 64, 32, 255};

  auto button_style = decltype(engine::ui::layout_builder::button_config::style){};
  button_style.container.normal.layout.sizing.width     = CLAY_SIZING_GROW(0, 0);
  button_style.container.normal.layout.sizing.height    = CLAY_SIZING_FIXED(32);
  button_style.container.normal.layout.childAlignment.x = CLAY_ALIGN_X_CENTER;
  button_style.container.normal.layout.childAlignment.y = CLAY_ALIGN_Y_CENTER;

  button_style.container.hovered = button_style.container.normal;
  button_style.container.active  = button_style.container.normal;

  button_style.container.normal.backgroundColor  = {32, 0, 0, 255};
  button_style.container.hovered.backgroundColor = {64, 0, 0, 255};
  button_style.container.active.backgroundColor  = {128, 0, 0, 255};

  button_style.label.normal.fontSize  = 16;
  button_style.label.normal.textColor = {255, 255, 255, 255};

  button_style.label.hovered = button_style.label.normal;
  button_style.label.active  = button_style.label.normal;

  auto input_style = decltype(engine::ui::layout_builder::input_config::style){};
  input_style.container.normal.layout.sizing.width     = CLAY_SIZING_GROW(0, 0);
  input_style.container.normal.layout.sizing.height    = CLAY_SIZING_FIXED(32);
  input_style.container.normal.layout.padding          = CLAY_PADDING_ALL(4);
  input_style.container.normal.layout.childAlignment.x = CLAY_ALIGN_X_LEFT;
  input_style.container.normal.layout.childAlignment.y = CLAY_ALIGN_Y_CENTER;
  input_style.container.normal.backgroundColor         = {16, 16, 16, 255};
  input_style.container.normal.border.width            = CLAY_BORDER_ALL(2);
  input_style.container.normal.border.color            = {96, 96, 96, 255};

  input_style.container.hovered = input_style.container.normal;
  input_style.container.active  = input_style.container.normal;

  input_style.container.hovered.border.color = {128, 128, 128, 255};
  input_style.container.active.border.color  = {64, 64, 196, 255};

  input_style.content.normal.fontSize  = 16;
  input_style.content.normal.textColor = {255, 255, 255, 255};

  input_style.content.hovered = input_style.content.normal;
  input_style.content.active  = input_style.content.normal;

  lb.begin_element({ .id = CLAY_ID("OuterContainer"), .style = root_style });
    lb.begin_element({ .id = CLAY_ID("SideBar"), .style = sidebar_style});
      lb.capture_mouse();

      if (lb.button({ .id = CLAY_ID("Item 1"), .label = CLAY_STRING("Item 1"), .style = button_style })) {
        printf("Item 1 clicked!\n");
      }

      if (lb.button({ .id = CLAY_ID("Item 2"), .label = CLAY_STRING("Item 2"), .style = button_style })) {
        printf("Item 2 clicked!\n");
      }

      if (lb.button({ .id = CLAY_ID("Item 3"), .label = CLAY_STRING("Item 3"), .style = button_style })) {
        printf("Item 3 clicked!\n");
      }

      lb.input({ .id = CLAY_ID("Input 1"), .style = input_style });

    lb.end_element();
  lb.end_element();
}

No more warnings, and no std::function idiocy... :tada:

linkdd avatar Jul 06 '25 22:07 linkdd

The correct order errors are mildly infuriating for me since most of the examples ignore the ones not used, yet my C++20 MSVC implementation complains and I have to constantly switch over to the clay source to see what order and which ones I'm missing.

cugone avatar Jul 17 '25 03:07 cugone

This is actually quite a big issue. It makes it very hard to use Clay with MSVC C++. While I know this is inherently a C library, a lot of C++ developers who use Visual Studio could benefit a lot from using Clay. Therefore, I suggest a solution to make C++ compliant.

The current CLAY macro implementation

#define CLAY(id, ...)                                                                                                                                               \
    for (                                                                                                                                                           \
        CLAY__ELEMENT_DEFINITION_LATCH = (Clay__OpenElementWithId(id), Clay__ConfigureOpenElement(CLAY__CONFIG_WRAPPER(Clay_ElementDeclaration, __VA_ARGS__)), 0);  \
        CLAY__ELEMENT_DEFINITION_LATCH < 1;                                                                                                                         \
        CLAY__ELEMENT_DEFINITION_LATCH=1, Clay__CloseElement()                                                                                                      \
    )
#define CLAY__CONFIG_WRAPPER(type, ...) (CLAY__INIT(CLAY__WRAPPER_TYPE(type)) { __VA_ARGS__ }).wrapped

As discussed already, this works great in C, but the CLAY_CONFIG_WRAPPER does not work nicely with C++ because of out of order field initializers.

Building a C++ style solution

The idea is to add a alternative CLAY_CONFIG_WRAPPER which creates a lambda, executes it and returns the result in a single statement. Then instead of using field initializers, the lambda provides an object that can be mutated within the curly braces of the original CLAY macro.

The first step is to expand the macro a bit to be a directly evaluated lambda expression instead of returning the result directly:

#define CLAY__CONFIG_WRAPPER(type, ...) [=]() -> type { type obj = (CLAY__INIT(CLAY__WRAPPER_TYPE(type)) { __VA_ARGS__ }).wrapped; return obj; }()

We use [=] to capture the entire scope. Simply replacing the original macro with this leaves its functionally unchanged so far. Now we move the VA_ARGS after the objects initialization:

#define CLAY__CONFIG_WRAPPER(type, ...) [=]() -> type { type obj = (CLAY__INIT(CLAY__WRAPPER_TYPE(type)) { }).wrapped; { __VA_ARGS__ } return obj; }()

obj is automatically hidden from scope in C++, so this supports nesting. Therefore, we can now write things like this in C++, even if initialization order is wrong:

CLAY(CLAY_ID("OuterContainer"), {
	obj.layout = CLAY__CONFIG_WRAPPER(Clay_LayoutConfig, {
		obj.sizing = layoutExpand;
		obj.padding = CLAY_PADDING_ALL(16);
		obj.childGap = 16;
		obj.layoutDirection = CLAY_TOP_TO_BOTTOM;
	});
	obj.backgroundColor = { 43, 41, 51, 255 };
});

It is a little bit different from its C counterpart, but it allows you to specify everything in whatever order you want. This makes development a lot easier.

Some things to note about this approach:

  • Portability: this approach is fully C++ compliant.
  • Internal compatibility: this breaks exactly one existing Clay macro: CLAY_PADDING_ALL. This can easily be rewritten to be compatible with both C and C++.
  • External compatibility: this fix would only be applied to C++ (C keeps its old CLAY_CONFIG_WRAPPER) with an option (maybe #ifdef CLAY_USE_FIELD_INITIALIZERS_CPP) to force the C macros on C++. So existing users of Clay in C++ only need to add a macro to port their code. Alternatively, this could be made opt-in instead of opt-out.

leetftw avatar Nov 28 '25 15:11 leetftw