Skip to content

Behavior of a user-defined conditional logical operator depends on order of true/false operator declarations #78602

@AlekseyTs

Description

@AlekseyTs

Compile and run the following code:

public class Program
{
    static void Main()
    {
        var x1 = new S1();
        var y1 = new S1();
        
        _ = x1 && y1;

        var x2 = new S2();
        var y2 = new S2();
        
        _ = x2 && y2;
    }
}

struct S1 // nullable operators come first
{
    public static S1 operator &(S1 x, S1 y) => x;
    public static bool operator true(S1? x) => true;
    public static bool operator false(S1? x)
    {
        System.Console.Write(1);
        return false;
    }
    public static bool operator true(S1 x) => true;
    public static bool operator false(S1 x) => false;
}

struct S2 // non-nullable operators come first
{
    public static S2 operator &(S2 x, S2 y) => x;
    public static bool operator true(S2 x) => true;
    public static bool operator false(S2 x)
    {
        System.Console.Write(2);
        return false;
    }
    public static bool operator true(S2? x) => true;
    public static bool operator false(S2? x) => false;
}

Observed:
The code outputs "12", which means that compiler chooses between the two available false operators based on their order of declaration.

The language spec says the following:

  • T shall contain declarations of operator true and operator false.
    ...
  • The operation x && y is evaluated as T.false(x) ? x : T.&(x, y), where T.false(x) is an invocation of the operator false declared in T, and T.&(x, y) is an invocation of the selected operator &. In other words, x is first evaluated and operator false is invoked on the result to determine if x is definitely false. Then, if x is definitely false, the result of the operation is the value previously computed for x. Otherwise, y is evaluated, and the selected operator & is invoked on the value previously computed for x and the value computed for y to produce the result of the operation.
  • The operation x || y is evaluated as T.true(x) ? x : T.|(x, y), where T.true(x) is an invocation of the operator true declared in T, and T.|(x, y) is an invocation of the selected operator |. In other words, x is first evaluated and operator true is invoked on the result to determine if x is definitely true. Then, if x is definitely true, the result of the operation is the value previously computed for x. Otherwise, y is evaluated, and the selected operator | is invoked on the value previously computed for x and the value computed for y to produce the result of the operation.

So, it doesn't explicitly say how to resolve the ambiguity, but one might assume that regular unary operator overload resolution rules should be used.

BTW, It looks like for S1 compiler emits an invalid code, see #78609. Perhaps compiler should be looking for operator true and operator false that take exactly the type taken by operator &/operator |, or the type that the type taken by operator &/operator | converts to through an implicit reference conversion?

Metadata

Metadata

Assignees

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions