Search code examples
c++ccross-platformvariable-assignmentlogical-operators

Logical AND + assignment in c++, safe?


I just learned this great pattern (from javascript actually) and I would like to apply it to my c++ code.

To explain the pattern, let's say I am representing a string as a linked list of these:

struct link_char;
struct link_char
{
   link_char * next;
   char code;
};

Note that the last character of any link_char string will always have code==0. This property means that I can check for a value in the string, while using && short-circuiting to prevent NULL pointer access.

bool equals_hello( const link_char * first_char )
{
    const link_char * c = first_char;

    return       c->code=='h' 
    && (c=c->next)->code=='e' 
    && (c=c->next)->code=='l' 
    && (c=c->next)->code=='l' // if string == "hel", we short-circuit here
    && (c=c->next)->code=='o';
}

My question is about safety, not readability. I know the short-circuiting will work as long as && is not overloaded. But will the assignment operations happen in the right order, or is it implementation defined?

The above example is explicit about where reads/writes can happen, but I would also like to use this pattern in situations where there can be side-effects. For example:

// think of these as a bunch of HRESULT type functions 
//   a return value of 0 means SUCCESS
//   a return value of non-zero yields an Error Message
int err;
( !(err=initialize()) && !(err=create_window()) && !(err=run_app() )
    || handle_error(err);

Will these kinds of operations work as intended cross-platform? I've read that "if you read a variable twice in an expression where you also write it, the result is undefined". But intuitively I feel like the short-circuiting guarantees the order, does it not?


Solution

  • Yes.

    Built-in logical AND (&&), logical OR (||) and the comma operator (,) are the only cases in which for a binary operator C++ guarantees that evaluation will compute left expression and then (if not short circuited) right expression (comma operator of course always evaluates both operands, first left and then right).

    Note also that the comma between function arguments is not a comma operator and therefore the order of evaluation of function arguments is not specified and even worse than that: for example in f(g(h()),i()) it is possible that the sequence of calls will be h,i,g,f.

    Also the guarantee about evaluation order only applies to built-in operators; if you redefine them then they basically become function calls where the order of evaluation of arguments is not guaranteed and where short-circuiting is not performed.

    Other binary operators don't guarantee the order of evaluation and for example one common mistake is to think that in:
    std::cout << foo() << bar();
    

    the call to foo() is guaranteed to happen before the call to bar() ... this is not true.

    (C++17 fixed this issue but only in a few very special cases, including the left shift operator because it's used for streams)

    Of course the order of evaluation is also guaranteed for the ternary :? operator, where only one of the two other expressions will be evaluated after first evaluating the condition.

    Another place in which the order of evaluation is guaranteed (and sometimes surprising for newbies) is member initialization lists for constructors, but in this case the order is not the one in the expression, but the order of member declaration in the class.... for example:

    struct Foo
    {
       int x, y;
       Foo() : y(compute_y()), x(compute_x()) {}
    };
    

    in this case it is guaranteed that the call compute_x() will be done BEFORE the call compute_y() because x precedes y in the member declarations.