KeyC0de
KeyC0de

Reputation: 5257

C Preprocessor Macro with 2 arguments issue

I have this macro in my C code:

#define ASSERT(ret, num) \  // make sure ret === num
        if (ret != num) { \
            fprintf(stderr, "Error [%d] at line [%d] in function [%s]. Date: [%s] Time: [%s]\n", \
                    ret, __LINE__, __FUNCTION__, __DATE__, __TIME__); \
            exit(ret); \
        }

Then i call it like so (all arguments are integers):

ASSERT(errNo, MPI_SUCCESS);
ASSERT(aliveNeighbors, 8);
ASSERT(newGrid, !NULL);

I get errors like (GCC v5.4):

expected identifier or ‘(’ before ‘if’
   if (ret != num) { \
error: stray ‘\’ in program
   ASSERT(errNo, MPI_SUCCESS);
error: stray ‘\’ in program
  ASSERT(aliveNeighbors, 8);
stray ‘\’ in program
  ASSERT(newGrid, !NULL);

What is wrong here?

Upvotes: 0

Views: 619

Answers (1)

Jonathan Leffler
Jonathan Leffler

Reputation: 753695

Given the macro definition:

#define ASSERT(ret, num) \  // make sure ret === num
        if (ret != num) { \
            fprintf(stderr, "Error [%d] at line [%d] in function [%s]. Date: [%s] Time: [%s]\n", \
                    ret, __LINE__, __FUNCTION__, __DATE__, __TIME__); \
            exit(ret); \
        }

you have a number of problems, many of them visible in your error messages.

The backslash-newline line splicing occurs in phase 2 of the processing defined in the standard (ISO/IEC 9899:2011) at §5.1.1.2 Translation phases:

  1. Physical source file multibyte characters are mapped, in an implementation-defined manner, to the source character set (introducing new-line characters for end-of-line indicators) if necessary. Trigraph sequences are replaced by corresponding single-character internal representations.

  2. Each instance of a backslash character (\) immediately followed by a new-line character is deleted, splicing physical source lines to form logical source lines. Only the last backslash on any physical source line shall be eligible for being part of such a splice. A source file that is not empty shall end in a new-line character, which shall not be immediately preceded by a backslash character before any such splicing takes place.

  3. The source file is decomposed into preprocessing tokens7) and sequences of white-space characters (including comments). A source file shall not end in a partial preprocessing token or in a partial comment. Each comment is replaced by one space character. New-line characters are retained. Whether each nonempty sequence of white-space characters other than new-line is retained or replaced by one space character is implementation-defined.

  4. Preprocessing directives are executed, macro invocations are expanded, and _Pragma unary operator expressions are executed. If a character sequence that matches the syntax of a universal character name is produced by token concatenation (6.10.3.3), the behavior is undefined. A #include preprocessing directive causes the named header or source file to be processed from phase 1 through phase 4, recursively. All preprocessing directives are then deleted.

(Phases 5-8 complete the processing but are not germane to this discussion.)

Note that comments are removed in phase 3, and the preprocessing proper occurs in phase 4, on tokenized input.

The first line of your macro definition is:

    #define ASSERT(ret, num) \  // make sure ret === num

This defines the body of the macro as \. You cannot place comments after the backslash that is to continue a macro, and you cannot use // … <eol> style comments in the body of a macro. If you added a backslash after the comment, it would continue the comment onto the next line, and the macro body would still be empty. You can use /* … */ comments with care:

    #define ASSERT(ret, num)   /* make sure ret === num */ \

This would work correctly. Because the original macro has the trailing // … <eol> comment, that completes the macro. The first error message is:

expected identifier or ‘(’ before ‘if’
   if (ret != num) { \

This is because the if line in the 'macro definition' is actually not a part of the macro, and the if is not expected in the context where it appears.

The other three errors are essentially the same:

error: stray ‘\’ in program
   ASSERT(errNo, MPI_SUCCESS);

The macro expanded to just a backslash, but you can't have stray backslashes in the text of a valid C program, hence the error message. When backslashes appear, they're always in stylized contexts with specific other characters following (digits, certain letters, certain punctuation symbols, newline).

You also have issues with the body of the macro:

  • __DATE__ and __TIME__ are the date and time when the code was compiled (preprocessed), not the date and time when the code was run. They're seldom useful, and not useful in this context. If you want date and time information for when the program is run, you'll define and call a function which determines and formats the current date/time.
  • __FUNCTION__ is not standard; __func__ is the standard predefined identifier.
  • As written, the macro cannot be safely used in some contexts, such as:

    if (alpha == omega)
        ASSERT(retcode, 0);
    else if (gamma == delta)
        gastronomic_delight(retcode, gamma, alpha);
    

    The else clause is incorrectly associated with the if in the (semi-corrected) ASSERT and not with the alpha == omega test.

Assembling these changes into a workable solution:

#define ASSERT(ret, num) \
            do { \
                if ((ret) != (num)) err_report(ret, __LINE__, __func__); \
            } while (0)

where you have:

extern _Noreturn void err_report(int err, int line, const char *func);

This function formats the message and reports it on standard error, determining time and date if appropriate. It also exits (hence the _Noreturn attribute — a feature of C11. You might prefer to #include <stdnoreturn.h> and use noreturn instead of _Noreturn. You might or might not decide to add __FILE__ as a value passed to the function; it would need another parameter.

Note, too, that the invocation:

ASSERT(newGrid, !NULL);

is suspect. It translates to if ((newGrid) == (!0)) which is not what you intended. You have to work a bit harder to test for not-null pointers:

ASSERT(newGrid != NULL, 1);

This compares the value in newGrid with NULL, generating 1 if the value is non-null, and 0 if it is null, and that is compared with the 1 argument. Note that the error number for this test would not be helpful — it would be 0.

If I've missed any crucial points in the comments, please let me know.

Upvotes: 3

Related Questions