Document Number: N2207
Submitter: Martin Sebor, Jonathan Wakely, Florian Weimer
Submission Date: March 26, 2018
Subject: Assert Expression Problematic For C++

Summary

The specification for the assert(scalar expression) macro contains the following in its description:

When it is executed, if expression (which shall have a scalar type) is false (that is, compares equal to 0), the assert macro writes information about the particular call that failed … on the standard error stream… It then calls the abort function.

It turns out that the parenthetical notes "which shall have a scalar type" and "compares equal to 0" are problematic for C++ as discussed in WG21 Library Working Group (LWG) issue 3011.

In C++, user-defined class types may be defined to act almost exactly like scalar types, or like other basic (or in C++ terminology, fundamental) types. By providing implicit conversion operators to the latter types they can be used almost entirely interchangeably. For example, objects of type Nifty defined below can be used in nearly all contexts where type bool is expected. In addition, the objects can be compared for equality to char*.

	struct Nifty {
	    operator bool () const;
	    bool operator== (const char*) const;
	};

An object of type Nifty can be used for example like this:

	extern Nifty x;
	if (x)   // converted to bool by Nifty::operator bool()
	    puts ("x is true");
	else
	    puts ("x is false");

or like this

	if (x == "123")   // compared to "123" via Nifty::operator==(const char*)
	    puts ("x equals 123");
	else
	    puts ("s does not equal 123");

With this as background, it should be clear that users of type Nifty would naturally expect to be able to use objects of the type in other similar contexts such as the assert macro.

However, because assert is specified to have the described effect when its argument "compares equal to 0", invoking it on a Nifty object would result not in a call to Nifty::operator bool() as its author no doubt intended and its users expect, but rather Nifty::operator==(const char*). Needless to say, it is surprising and thus error-prone for C++ programmers accustomed to C++ rules to have to keep in mind that such an essential facility of the language as the assert macro doesn't play by the same rules. Fedora Linux bug 1482990 provides evidence of this impact.

It might seem that this is a C++ problem, not one for the C standard or its implementation to try to solve. However, similar to POSIX, the C++ standard incorporates the library portion of the C standard largely without change. Likewise, virtually all C++ standard library implementations incorporate the underlying C standard library by reference, often with no ability to make changes or customizations to it. So to solve the problem for C++ the only realistic solution is to compel C standard library implementers to make a change by adjusting the C specification.

Proposed Resolution

In the interest of interoperability between C and C++ we propose to change the specification of assert as follows. In the Synopsis paragraph of §7.2.1.1 The assert macro make the following changes:

Synopsis
	#include <assert.h>
	void assert(scalar expression);

In paragraph 2 of the same §7.2.1.1 make the following changes:

The assert macro puts diagnostic tests into programs; it expands to a void expression. When it is executed, if expression (which shall have a scalar type be implicitly convertible to _Bool) is false (that is, compares equal to 0 (expression ? true : false) evaluates to non-zeronew-footnote), the assert macro writes information about the particular call that failed (including the text of the argument, the name of the source file, the source line number, and the name of the enclosing function—the latter are respectively the values of the preprocessing macros __FILE__ and __LINE__ and of the identifier __func__) on the standard error stream in an implementation-defined format. 191) It then calls the abort function.

new-footnote) This form was deliberately chosen for compatibility with C++ and its rules for contextual conversion to bool. Implementations are encouraged to consider such compatibility when using different forms of expressions.

Finally, change the following bullet from §J.2 Undefined behavior as indicated:

The argument to the assert macro is not implicitly convertible to _Booldoes not have a scalar type (7.2).

Besides addressing the C/C++ interoperability problem the proposed removal of the reference to "scalar type" in the synopsis and in the first parenthetical note permits the use of arrays and atomic types as assert arguments. A strict reading of the text suggests that objects of those types are not valid arguments in C, but arrays are valid arguments in C++. We believe that allowing them is an improvement since using such arguments is not uncommon in practice, and we are not aware of any reason to preclude them.

We note that objects of atomic types don't appear to be valid operands to the ternary operator. That seems like a defect in the standard.