3

I've run into a somewhat strange issue. It makes me feel like the answer is blaringly obvious and I'm just not seeing something because the code is so simple.

I basically have a macro called "ASSERT" that makes sure a value isn't false. If it is, it writes a message to the console and debug breaks. My issue is that when asserting that the index returned from std::string::find_first_of(...) is not equal to std::string::npos, the assert doesn't seem to work at all. Every time, the assertion fails.

I have verified that the values are not equal when the debug break occurs, so I don't see how the assertion is failing.

In my original code, it reads data from a file to a string, but the issue still seems to be present in the example below with nothing but a const std::string.

I'm working on a much bigger project, but here's a minimal example that reproduces the bug (I'm using Visual Studio 2022 and C++17 by the way):

#include <iostream>
#include <string>

#define ASSERT(x, msg) {if(!x) { std::cout << "Assertion Failed: " << msg << "\n"; __debugbreak(); } }

int main() {

    const std::string source = "Some string that\r\n contains the newline character: \r\n...";

    size_t eol = source.find_first_of("\r\n", 0);
    ASSERT(eol != std::string::npos, "Newline not present!");
    
    // Other code...
    
    return 0;
}

Please note that the exact same thing happens even when only one newline character ("\r\n") is present in the string.

What's interesting is that "eol" seems to have the correct value in every test case that I've run. The only thing that's wrong is the assertion, so if I ignore it and continue, everything runs exactly how I expect it to.

I also found this issue that seems related, but no answer or conclusion was reached: std::string::find_first_of does not return the expected value

cigien
  • 57,834
  • 11
  • 73
  • 112
CodeMan
  • 99
  • 6
  • 4
    Try `#define ASSERT(x, msg) {if(!(x)) { ... } }` – 0x5453 Jan 07 '22 at 22:27
  • 2
    Turn on all compiler warnings. That macro should generate at least half a dozen, including "note: add parentheses around left hand side expression to silence this warning". – Passerby Jan 07 '22 at 22:28
  • Unrelated: make sure that \r\n isn't being translated into a single newline character. – user4581301 Jan 07 '22 at 22:30
  • 1
    Visual Studio's warning messages for this are pretty sad. GCC was much more helpful: https://godbolt.org/z/nv3Mavcb1 I keep a few different compilers around to see if one sees mistakes that others missed, and Matt Godbolt's compiler Explorer... That's the bomb for quickly comparing the results of small pieces of code. – user4581301 Jan 07 '22 at 22:40
  • 3
    Please don't delete the question. I've edited the title to describe the error more clearly, and be more searchable. This may be a duplicate, but it's still a useful question. – cigien Jan 07 '22 at 22:40
  • 1
    Like I said: well-posed question. Anyone confused by it needs more help than we can offer here. – user4581301 Jan 07 '22 at 22:41
  • Better to use something like `if (!(x)) { ... } else ((void)0)` so that missing ; before a nested else won't result in unexpected flow. – Phil1970 Jan 07 '22 at 23:54

2 Answers2

2

An answer has been provided in the comments. The solution is to simply add parenthesis around the x in the "!x" portion of the macro. This translates to the following (credit to 0x5453):

#define ASSERT(x, msg) {if(!(x)) { ... } }

The reason that this fixes the problem is that if you expand the original macro, it'll look something like this (this is how it is compiled):

if(!eol != std::string::npos) {...}

This is pretty obviously incorrect because you want to check that the entire condition is false. Like this:

if(!(eol != std::string::npos)) {...}

Adding the parentheses fixes this.

Simple, but still a good reminder to check the little things like this.

CodeMan
  • 99
  • 6
2

This is the unexpected consequence of the by-design simple stupidity of the preprocessor's macro substitution engine. An expression supplied to the macro is not evaluated as it would be with a function, the text is inserted directly during substitution.

Given

#define ASSERT(x, msg) {if(!x) { std::cout << "Assertion Failed: " << msg << "\n"; __debugbreak(); } }

the line

ASSERT(eol != std::string::npos, "Newline not present!");

will be transformed into

{if(!eol != std::string::npos) { std::cout << "Assertion Failed: " << "Newline not present!" << "\n"; __debugbreak(); } }

and the ! is only applied to the eol, changing the expected behaviour of the macro to something nonsensical.

Adding the extra brackets recommended in the comments

#define ASSERT(x, msg) {if(!(x)) { std::cout << "Assertion Failed: " << msg << "\n"; __debugbreak(); } }

results in

{if(!(eol != std::string::npos)) { std::cout << "Assertion Failed: " << "Newline not present!" << "\n"; __debugbreak(); } }

and now the expression is being evaluated before applying the ! and tested.

Because macros are "evil", and since the macro makes no use of any special, position dependent debugging macros like __FILE__ or __LINE__, this is a case where I would replace the macro with a function and count on the compiler's optimizations to inline it.

user4581301
  • 33,082
  • 7
  • 33
  • 54
  • I was editing my answer just now to say this. I like your answer though because it not only explains it better, but I can't accept my own answer until two days from now. Thanks and cheers! – CodeMan Jan 07 '22 at 23:14