Home > Back-end >  Macros not expanding properly
Macros not expanding properly

Time:07-08

I have these macros defined:

#define AL_ASSERT_NO_MSG(x) if (!(x)) { assertDialog(__LINE__, __FILE__); __debugbreak(); }
#define AL_ASSERT_MSG(x, msg) if (!(x)) { assertDialog(msg, __LINE__, __FILE__); __debugbreak(); }

#define GET_MACRO(_1, _2, NAME, ...) NAME
#define AL_ASSERT(...) GET_MACRO(__VA_ARGS__, AL_ASSERT_MSG, AL_ASSERT_NO_MSG)(__VA_ARGS__)

I wanted to have it dispatched to the AL_ASSERT_NO_MSG macro if I only pass one argument and AL_ASSERT_MSG if I pass two arguments.

However, when I use the macro like this: AL_ASSERT(false, "Test") it expanded to if (!(false, "Test")) { assertDialog(23, "C:\\Dev\\c \\SortingAlgorithms\\AlgorithmVisualizer\\src\\Window.cpp"); __debugbreak(); }; and it does not work.

More information: I am using Visual Studio with MSVC. This is not a solution-based project but a CMake project opened as a folder.

What did I do wrong? Any help is appreciated.

CodePudding user response:

You only need the macro because of the use of __LINE__ and __FILE__. All the other logic can be done in a usual function:

// Required arguments for args:
// message [optional]
// __LINE__
// __FILE__
template<typename C, typename... Args>
constexpr void al_assert(C&& condition, Args&&... args) {
   static_assert(sizeof...(args) >= 2);
   static_assert(sizeof...(args) <= 3);
   if (!std::forward<C>(condition)) {
       assertDialog(std::forward<Args>(args)...);
       __debugbreak();
   }
}

// assuming al_assert is placed in global namespace scope
// but better use a private namespace for al_assert
#define AL_ASSERT(...) (::al_assert(__VA_ARGS__, __LINE__, __FILE__))

// or
// #define AL_ASSERT(...) do { ::al_assert(__VA_ARGS__, __LINE__, __FILE__); } while(0)

With C 20 the macro is not required at all:

#include<source_location>

//...

template<typename C, typename... Args>
struct al_assert {
    constexpr al_assert(C&& condition, Args&&... args,
                        std::source_location location = std::source_location::current()) {
       static_assert(sizeof...(args) <= 1);
       if (!std::forward<C>(condition)) {
           assertDialog(std::forward<Args>(args)..., location.line(), location.file_name());
           __debugbreak();
       }
    }
};

template<typename C, typename... Args>
al_assert(C&&, Args&&...) -> al_assert<C, Args...>;

This can be used directly instead of using the macro as al_assert(/*...*/). The CTAD trick to make std::source_location after the parameter pack work is due to this answer.

(The template parameters and forwarding references are there to cover potential edge cases to make it as close as possible to the macro behavior. Depending on your use case, you may be happy with e.g. making the condition simply a bool instead.)

CodePudding user response:

It seems that MSVC does not expand __VA_ARGS__ into multiple arguments (see this related question: MSVC doesn't expand __VA_ARGS__ correctly)

This seems to work:

#define GET_MACRO(_1, _2, NAME, ...) NAME
#define XGET_MACRO(args) GET_MACRO args
#define AL_ASSERT(...) XGET_MACRO((__VA_ARGS__, AL_ASSERT_MSG, AL_ASSERT_NO_MSG))(__VA_ARGS__)

And if you are targeting C 20 (and have Visual Studio 2019 version 16.5), you can use __VA_OPT__:

#define AL_ASSERT(x, ...) if (!(x)) { assertDialog(__VA_OPT__( (__VA_ARGS__) , ) __LINE__, __FILE__); __debugbreak(); }

Or you can do it with some functions:

#define AL_ASSERT(x, ...) (([](bool b, const auto*... msg) { \
    static_assert(sizeof...(msg) <= 1, "AL_ASSERT passed too many arguments"); \
    if (!b) {                                     \
        if constexpr (sizeof...(msg) == 1) {      \
          const char* m{msg...};                  \
          assertDialog(msg, __LINE__, __FILE__);  \
        } else {                                  \
          assertDialog(__LINE__, __FILE__);       \
        }                                         \
        __debugbreak();
})(x, __VA_ARGS__))

And with version 16.10 you can do it without a macro with std::source_location:

void AL_ASSERT(bool x, const std::source_location location = std::source_location::current()) {
    if (!x) {
        assertDialog(location.line(), location.file_name());
        __debugbreak();
    }
}

void AL_ASSERT(bool x, const char* msg, const std::source_location location = std::source_location::current()) {
    if (!x) {
        assertDialog(msg, location.line(), location.file_name());
        __debugbreak();
    }
}

CodePudding user response:

Trying to separate the expression from the message using preprocessor is not the best idea, since both can contain commas, making it impossible in general.

But you can do this:

void foo(int line, bool cond, std::string msg_or_file, const char *file_or_null = nullptr);

#define ASSERT(...) foo(__LINE__, __VA_ARGS__, __FILE__)

Then, if you do provide a mesasge, foo(line, cond, "message", "file") will be called, and otherwise foo(line, cond, "file", nullptr) will be called.

By checking if the last parameter is zero, you see whether the third parmaeter is a message or a filename.

... Or you can just overload the function.

  •  Tags:  
  • c
  • Related