C++ Logo

std-proposals

Advanced search

[std-proposals] Manifold comparison operator

From: Ben Crowhurst <ben.crowhurst_at_[hidden]>
Date: Thu, 07 Sep 2023 09:03:02 +0000
We propose a new range of conditional operators for C++:

* [^] one-of
* [&] all-of
* [|] any-of
* [!] none-of

These operators simplify common code patterns and improve the clarity of basic conditional expressions.


1 ) Before/After
Sample code is taken from the llvm-project (clang/lib/StaticAnalyzer/Checkers/SmartPtrModeling.cpp).

Before the proposal:

    const OverloadedOperatorKind OOK = FD->getOverloadedOperator();
    if (!(OOK == OO_EqualEqual || OOK == OO_ExclaimEqual || OOK == OO_Less ||
          OOK == OO_LessEqual || OOK == OO_Greater || OOK == OO_GreaterEqual ||
          OOK == OO_Spaceship))
       return false;

After the proposal:

    const OverloadedOperatorKind OOK = FD->getOverloadedOperator();
    if (OOK [!] OO_EqualEqual, OO_ExclaimEqual, OO_Less, OO_LessEqual, OO_Greater,
                OO_GreaterEqual, OO_Spaceship)
        return false;

Resulting in a 21.7% decrease in character count and a reduction in cognitive load, with no requirement to study each comparison operator delimited by subsequent logical operators.


2) Proposal
There is one conditional operator in the C++ standard :? (Ternary). We propose to expand this horizon with four additional operators.

  Operator Statement Equivalence

  one of if (var [^] val1, val2) E; if ((var == val1 && var != val2) || (var != val1 && var == val2)) E;

  all of if (var [&] val1, val2) E; if (var == val1 && var == val2) E;

  any of if (var [|] val1, val2) E; if (var == val1 || var == val2) E;

  none of if (var [!] val1, val2) E; if (var != val1 && var != val2) E;

As is the case with the Ternary operator, no overloading will be supported. This ensures that conditional operands are only evaluated based on the truth or falseness of the conditional expression.

The Comma operator will NOT be interpreted during the evaluation of the right-hand side of a Manifold operator.


3) Motivation
To improve code clarity when interpreting multifaceted conditionals.


4) Alternatives
A number of alternative solutions exist, however, all inflict increasing code complexity and distract from interpreting the program control flow.

4.1) Macro solution

  #define NONE_OF0(lhs, rhs, ...) lhs != rhs
  #define NONE_OF1(lhs, rhs, ...) lhs != rhs && NONE_OF0(lhs, __VA_ARGS__)
  // Add NONE_OF2, 3, 4, 5... as desired.
  #define NONE_OF(lhs, ...) NONE_OF1(lhs, __VA_ARGS__)

  const OverloadedOperatorKind OOK = FD->getOverloadedOperator();
  if (NONE_OF(OOK, OO_EqualEqual, OO_ExclaimEqual, OO_Less, OO_LessEqual,
                   OO_Greater, OO_GreaterEqual, OO_Spaceship))
      return false;

A macro solution is hard to debug, verbose, specific to the desired number of potential arguments, and fails to clearly communicate the intent; comparison LHS appears as the first argument.

4.2) Template solution

    template <typename LHS, typename... Args>
    auto none_of(LHS lhs, Args... args)
    {
        const auto predicate = std::not_equal_to<LHS>();
        return (predicate(lhs, args) && ...);
    }

    const OverloadedOperatorKind OOK = FD->getOverloadedOperator();
    if (none_of(OOK, OO_EqualEqual, OO_ExclaimEqual, OO_Less, OO_LessEqual,
                     OO_Greater, OO_GreaterEqual, OO_Spaceship))
        return false;

Much improved over the macro solution, yet fails to clearly communicate the intent without understanding the internals; comparison LHS appears as the first argument.

4.3) STL solution
std::all_of, std::any_of, std::none_of and std::ranges equivalent.

    const std::set<OverloadedOperatorKind> ComparisonOperators = {
        OO_EqualEqual, OO_ExclaimEqual, OO_Less, OO_LessEqual, OO_Greater,
        OO_GreaterEqual, OO_Spaceship
    };

    const OverloadedOperatorKind OOK = FD->getOverloadedOperator();
    if (std::none_of(std::cbegin(ComparisonOperators),
                     std::cend(ComparisonOperators),
                     [&](auto op) { return op == OOK; }))
        return false;

The STL solution presents a verbose conditional construction and is not easily capable of handling variables, literals, and functional call-sites without increasing code complexity. Additionally, there is no support for std::one_of.

4.4) Helper function solution

    const OverloadedOperatorKind OOK = FD->getOverloadedOperator();
    if (is_not_comparison_operator(OOK))
        return false;

Provides a clean and concise meaning towards control flow, however, it results in the proliferation of many helper functions to address all possible combinations throughout a codebase e.g. is_comparison_operator, is_equality_operator, is_not_equality_operator, etc.

In more involved scenarios this approach becomes unwieldy, as dynamic aspects must be passed into the function as arguments or expansion of the condition must occur i.e.

    const OverloadedOperatorKind OOK = FD->getOverloadedOperator();
    if (is_not_comparison_operator(OOK) && OOK != NextToken().getKind())
        return false;


5) Impact on the Standard
The newly proposed syntax is ill-formed in the current working draft. This is a core language extension.


6) Proposed wording
Section '7.6 Compound expressions' requires the addition of new grammar and wording detailing the Manifold operator syntax. TBD.


7) Implementation

A rudimentary implementation was crafted into the llvm-project to determine the impact on existing compiler infrastructure.

* Lex library - Addition of new manifold tokens.
* Parse library - Modification of binary expression parsing to emit equivalent entries into the AST.

Minimal changes were required.


Thank you for your time.

Regards,
Ben Crowhurst

Received on 2023-09-07 09:03:17