In the process of investigating a .NET API proposal, to add the new System.Numerics
interfaces to System.Boolean
, it transpired that &&
and ||
short-circuiting operators need the operators true
and false
to be implemented.
I tried to test doing so with an interface containing the static abstract
operators (a new feature for .NET 7):
public interface ITruthOperators<TSelf>
where TSelf : ITruthOperators<TSelf>?
{
static abstract bool operator true(TSelf left);
static abstract bool operator false(TSelf left);
static abstract TSelf TruthyIdentity { get; }
static abstract TSelf FalseyIdentity { get; }
}
And a custom boolean type:
public struct MyBool(bool val) : ITruthOperators<MyBool>, IBitwiseOperators<MyBool, MyBool, MyBool>
{
public bool Value {get;} = val;
public static bool operator true(MyBool b) => b.Value;
public static bool operator false(MyBool b) => b.Value;
public static MyBool TruthyIdentity => new(true);
public static MyBool FalseyIdentity => new(false);
public static MyBool operator &(MyBool a, MyBool b) => new(a.Value & b.Value);
public static MyBool operator |(MyBool a, MyBool b) => new(a.Value | b.Value);
public static MyBool operator ^(MyBool a, MyBool b) => new(a.Value ^ b.Value);
public static MyBool operator ~(MyBool val) => new(!val.Value);
}
You can now write something like if (T.TruthyIdentity)
or if (!someMyBool)
. You can also use non-short-circuiting operators someMyBool & someOtherMyBool
But using short-circuiting operators still does not work.
public T IsTrue<T>(T a, T b) where T : ITruthOperators<T>, IBitwiseOperators<T>
{
return a && b;
}
just gets you:
In order to be applicable as a short circuit operator a user-defined logical operator ('IBitwiseOperators<T, T, T>.operator &(T, T)') must have the same return type and parameter types
This makes no sense, because the operator &
defined on IBitwiseOperators<TSelf,TOther,TResult>
is defined as
public static abstract TResult operator & (TSelf left, TOther right);
so if you re-ify the generic type as IBitwiseOperators<MyBool, MyBool, MyBool>
then you get:
public static abstract MyBool operator & (MyBool left, MyBool right);
I tried to redefine my own IBitwiseOperators<TSelf>
where all the parameters and return types are the same generic parameter, in case that was the issue.
public interface IBitwiseOperators<TSelf>
where TSelf : IBitwiseOperators<TSelf>?
{
static abstract TSelf operator &(TSelf left, TSelf right);
static abstract TSelf operator |(TSelf left, TSelf right);
static abstract TSelf operator ^(TSelf left, TSelf right);
static abstract TSelf operator ~(TSelf value);
}
This got a different error:
In order for 'IBitwiseOperators<T>.operator &(T, T)' to be applicable as a short circuit operator, its declaring type 'IBitwiseOperators<T>' must define operator true and operator false
Which again makes no sense, because T
in the function is constrained to both IBitwiseOperators
and ITruthOperators
.
The only way I could get this to work was if I placed all the operators in the same interface.See this fiddle for that attempt which finally worked.
Note the definition for short-circuiting operators in the C# Spec:
When the operands of
&&
or||
are of types that declare an applicable user-defined operator&
or operator|
, both of the following shall be true, whereT
is the type in which the selected operator is declared:
- The return type and the type of each parameter of the selected operator shall be
T
. In other words, the operator shall compute the logical AND or the logical OR of two operands of typeT
, and shall return a result of typeT
.T
shall contain declarations of operatortrue
and operatorfalse
.A binding-time error occurs if either of these requirements is not satisfied. Otherwise, the
&&
or||
operation is evaluated by combining the user-definedoperator true
oroperator false
with the selected user-defined operator:etc…
What do the words “where T
is the type in which the selected operator is declared” mean here? Does that mean the interface, or does it mean the generic type? I think it should mean the generic type. Remember we are dealing with a generic type that is constrained to those interfaces, the variables we are handling are actually of type T
.
So what I’m essentially asking is: is this a bug in the compiler, or is this by design?