A Deep Dive Into Boolean Operator Overloading

Posted by : on

Category : C#

Introduction

Some time ago, I wrote a post about How the boolean logical operators are used and get overloaded in C#. This post delves deeper into the true and false operators, the negation operator, and their connection to short-circuit (or conditional) logical operators.

In this post, I will implement a type that represents Schrödinger’s cat. This type will have the unique capability of returning both true and false when the cat is considered to be inside the box. We’ll explore how to implement custom logical operators, observe the behavior of conditional logic based on these custom implementations, and examine potential issues that may arise from conditional logic.

While I can’t speak to how meaningful Boolean logic is in physics for objects in superposition, this post isn’t about physics. Schrödinger’s cat simply serves as a fun premise for implementing a type where the values true and false aren’t the only Boolean results possible. In this case, a nullable Boolean isn’t an option because we aren’t dealing with traditional three-way logic (true, false, or neither); instead, we have an object that can be both true and false simultaneously.

Overloading Boolean Operators

In C#, true and false can refer to two things: they can represent literal Boolean values or they can be the true and false operators themselves.

Additionally, we have two ways to perform Boolean AND/OR operations: the regular logical operators & and |, and their conditional counterparts && and ||. The conditional versions cannot be overloaded, as they rely on the logical operators and the true/false operators to compute a result.

Lastly, there’s the negation operator !, which computes the logical negation of its operand. This means that unless it’s overloaded, it will return true if the operand is false, and false if the operand is true.

Our goal is to create a SchrodingerCat type that will return true when the cat is alive, false when the cat is dead, and both true and false when the cat is inside the box. This can be achieved in two ways: by overloading the implicit conversion of our type to bool, or by overloading the true and false operators. Finally we have to be careful to check for the commutative property of our conditional boolean operators.

Schrodinger’s Cat Implementation

Let’s start with a basic implementation without overloading any of the operators yet:

public class SchrodingerCat
{
   private bool _isAlive = true;
   private bool _isBoxOpen = true;
   private readonly Random _rnd = new();
   private readonly CancellationTokenSource _cancellationTokenSource = new();
   private readonly int _testDelay;

   public SchrodingerCat(int testDelay) => _testDelay = testDelay;
   public SchrodingerCat() => _testDelay = _rnd.Next(0, int.MaxValue);

   public async Task CloseInsideBox()
   {
      _isBoxOpen = false;

      await Task.Delay(_testDelay, _cancellationTokenSource.Token);
      
      if(!_cancellationTokenSource.IsCancellationRequested)
         _isAlive = false;
   }

   public void OpenBox()
   {
      _cancellationTokenSource.Cancel();
      _isBoxOpen = true;
   }

   private bool IsAlive()
   {
      if (!_isBoxOpen) return true;
      return _isAlive;
   }
   
   private bool IsDead()
   {
      if (!_isBoxOpen) return true;
      return !_isAlive;
   }
}

Here we have two constructors. The parameterless constructor is the one that represents a Schrodinger’s cat, the other exists for testing purposes, as it allows us to control when the cat will die inside the box.

We have two important boolean variables: the _isAlive that represents if the cat is alive and the _isBoxOpen that represents if the box that we have inserted the cat is open or closed.

The CloseInsideBox method, will make the _isBoxOpen false and then after waiting for some time, will make the _isAlive variable false too. The OpenBox method, makes the _isBoxOpen true again and cancels the CloseInsideBox operations. If the cat is still alive when the box is opened then it will stay alive.

These two boolean variables are responsible for the boolean value of our SchrodingerCat type. If the _isBoxOpen is true, the type will be equal to the _isAlive variable. If the _isBoxOpen is false then our type should be both true and false. This means that when the box is closed, regardless of the _isAlive variable, both if(cat) anf if(!cat) should execute.

The IsAlive method returns true if the _isBoxOpen is false, else it returns the _isAlive variable. The IsDead method returns true if the _isBoxOpen is false, else it returns the opposite of the _isAlive variable.

Simple enough up to this point, now we have to choose how we want to implement the operators that will be responsible for the boolean values and logic of our type. Here we have different options:

We can just implement the implicit operator to bool. This is straightforward:

 public static implicit operator bool(SchrodingerCat cat) => cat.IsAlive();

Now by executing:

SchrodingerCat cat = new(1_000_000);

cat.CloseInsideBox();

if(cat) Console.WriteLine("The Cat is alive");
if(!cat) Console.WriteLine("The Cat is dead");

Console.ReadLine();

cat.OpenBox();

if (cat) Console.WriteLine("The Cat is alive");
if (!cat) Console.WriteLine("The Cat is dead");

We will get:

The Cat is alive

The Cat is alive

This is not ok, we have to also override the negation operator !:

public static bool operator !(SchrodingerCat cat) => cat.IsDead();

Now the above will give us:

The Cat is alive
The Cat is dead

The Cat is alive

We can even do:

if (cat && !cat) 
    Console.WriteLine("The Cat is both alive and dead!");

Which will return The Cat is both alive and dead! before the box is opened.

From a boolean logic side of view, we are ok, but for the implementation of a type that can be both true and false, doesn’t mean that the requirements are the traditional boolean logic operations. For example, our type right now, for the following operations:

SchrodingerCat cat = new(1_000_000);
SchrodingerCat cat2 = new(0);

cat.CloseInsideBox();
cat2.CloseInsideBox();

if 
   (cat && !cat) Console.WriteLine("The Cat is both alive and dead!");
else
{
   if(cat) Console.WriteLine("The Cat is alive");
   if(!cat) Console.WriteLine("The Cat is dead");
}

if 
   (cat2 && !cat2) Console.WriteLine("The Cat2 is both alive and dead!");
else
{
   if (cat2) Console.WriteLine("The Cat2 is alive");
   if (!cat2) Console.WriteLine("The Cat2 is dead");
}

if (cat & cat2) Console.WriteLine("& operator returns true");
else Console.WriteLine("& operator returns false");

if (cat && cat2) Console.WriteLine("&& operator returns true");
else Console.WriteLine("&& operator returns false");

if (cat2 && cat) Console.WriteLine("&& operator reversed returns true");
else Console.WriteLine("&& operator reversed returns false");

if (cat | cat2) Console.WriteLine("| operator returns true");
else Console.WriteLine("| operator returns false");

if (cat || cat2) Console.WriteLine("|| operator returns true");
else Console.WriteLine("|| operator returns false");

if (cat2 || cat) Console.WriteLine("|| operator reversed returns true");
else Console.WriteLine("|| operator reversed returns false");

Console.ReadLine();

cat2.OpenBox();

if 
   (cat && !cat) Console.WriteLine("The Cat is both alive and dead!");
else
{
   if(cat) Console.WriteLine("The Cat is alive");
   if(!cat) Console.WriteLine("The Cat is dead");
}

if 
   (cat2 && !cat2) Console.WriteLine("The Cat is both alive and dead!");
else
{
   if (cat2) Console.WriteLine("The Cat2 is alive");
   if (!cat2) Console.WriteLine("The Cat2 is dead");
}

if (cat & cat2) Console.WriteLine("& operator returns true");
else Console.WriteLine("& operator returns false");

if (cat && cat2) Console.WriteLine("&& operator returns true");
else Console.WriteLine("&& operator returns false");

if (cat2 && cat) Console.WriteLine("&& operator reversed returns true");
else Console.WriteLine("&& operator reversed returns false");

if (cat | cat2) Console.WriteLine("| operator returns true");
else Console.WriteLine("| operator returns false");

if (cat || cat2) Console.WriteLine("|| operator returns true");
else Console.WriteLine("|| operator returns false");

if (cat2 || cat) Console.WriteLine("|| operator reversed returns true");
else Console.WriteLine("|| operator reversed returns false");

Would give:

The Cat is both alive and dead!
The Cat2 is both alive and dead!
& operator returns true
&& operator returns true
&& operator reversed returns true
| operator returns true
|| operator returns true
|| operator reversed returns true

The Cat is both alive and dead!
The Cat2 is dead
& operator returns false
&& operator returns false
&& operator reversed returns false
| operator returns true
|| operator returns true
|| operator reversed returns true

This happens because the compiler makes an implicit conversion to boolean for our cat type, so the results are the results of boolean logic. If that’s desirable we are ok, but if such a type has different requirements, then overloading the implicit boolean operator is not desirable. Instead we should overload the true and false operators.

The True And False Boolean Operators

If in an operation that needs a boolean variable (if, while, for, do) there is not an implicit conversion to bool, the compiler will check if the type implements the true and false operators before showing an error.

If the true operator returns true, then the object of the type has a true literal value otherwise it has false. If the false operator returns true, then it has a false literal value, else it has true.

Both of those operators have to be implemented, that’s a requirement from the compiler, but those operators don’t have to complement each other. We can have states in our type that are not true or false and that’s better implemented with a nullable boolean type, or we can have states, like in our case, that are considered both true and false. This means that in these states, the true operator will return true and the false operator will also return true.

There’s a caveat. The false operator, is only useful for calculating the conditional AND operator, in all other cases the true operator is used or the negation operator.

Let’s suppose that our type now is implemented like this:

public class SchrodingerCat
{
   private bool _isAlive = true;
   private bool _isBoxOpen = true;
   private readonly Random _rnd = new();
   private readonly CancellationTokenSource _cancellationTokenSource = new();
   private readonly int _testDelay;

   public SchrodingerCat(int testDelay) => _testDelay = testDelay;
   public SchrodingerCat() => _testDelay = _rnd.Next(0, int.MaxValue);

   public async Task CloseInsideBox()
   {
      _isBoxOpen = false;

      await Task.Delay(_testDelay, _cancellationTokenSource.Token);
      
      if(!_cancellationTokenSource.IsCancellationRequested)
         _isAlive = false;
   }

   public void OpenBox()
   {
      _cancellationTokenSource.Cancel();
      _isBoxOpen = true;
   }

   private bool IsAlive()
   {
      if (!_isBoxOpen) return true;
      return _isAlive;
   }
   
   private bool IsDead()
   {
      if (!_isBoxOpen) return true;
      return !_isAlive;
   }
   
   public static bool operator true(SchrodingerCat cat) => cat.IsAlive();

   public static bool operator false(SchrodingerCat cat) => cat.IsDead();
}

We can have the following:

SchrodingerCat cat = new(1_000_000);

cat.CloseInsideBox();

if(cat) Console.WriteLine("The Cat is alive");

But not this:

if(!cat) Console.WriteLine("The Cat is dead");

If we want the above then we should override the negation operator in our type:

public static bool operator !(SchrodingerCat cat) => cat.IsDead();

Now the following:

SchrodingerCat cat = new(1_000_000);

cat.CloseInsideBox();

if(cat) Console.WriteLine("The Cat is alive");
if(!cat) Console.WriteLine("The Cat is dead");

Console.ReadLine();

cat.OpenBox();

if(cat) Console.WriteLine("The Cat is alive");
if(!cat) Console.WriteLine("The Cat is dead");

Will have a result:

The Cat is alive
The Cat is dead

The Cat is alive

Unfortunately the following will not compile:

if(cat && !cat) Console.WriteLine("The Cat is both alive and dead!");

The reason is that we try to use the AND operator between two different types, a SchrodingerCat type and a boolean type, but we haven’t defined an AND operation that has those two types as parameters.

Instead of later creating a mess with all the available combinations in logical operations between those two types, it is better to keep boolean literal values only in the return types of the true and false overloaded operators and change the implementation of the negation overload to return a SchrodingerCat type.

public static SchrodingerCat operator !(SchrodingerCat cat)
{
    SchrodingerCat tempCat = new SchrodingerCat();
      
    if (!cat._isBoxOpen)
    {
        tempCat._isBoxOpen = false;
        return tempCat;
    }

    tempCat._isAlive = !cat._isAlive;
    return tempCat;
}

Now the previous still doesn’t compile, but for a different reason: The operator AND is not defined for our type.

Overriding The Implicit Conversion of Bool To SchrodingerCat

Another option here would be to define the implicit conversion from bool to SchrodingerCat like this:

public static implicit operator SchrodingerCat(bool boolean)
{
    SchrodingerCat tempCat = new SchrodingerCat();
    if (boolean) return tempCat;
    else
    {
        tempCat._isAlive = false;
        return tempCat;
    }
}

But this is not desirable, as it doesn’t really mean anything for our type. A conversion from bool to SchrodingerCat would be difficult to have any conceptual meaning.

Overriding The Logical And/Or Operators

Now that we have defined the negation operator in a way that returns a SchrodingerCat type, we can implement the AND and OR operators (&,|) in a way that they don’t have any booleans but both parameters and the return type is SchrodingerCat.

public static SchrodingerCat operator &(SchrodingerCat cat1, SchrodingerCat cat2)
{
   if (!cat1._isBoxOpen && !cat2._isBoxOpen) return cat1;
   
   if (!cat1._isBoxOpen && cat2 is { _isBoxOpen: true, _isAlive: true }) return cat2;
   
   if(!cat2._isBoxOpen && cat1 is { _isBoxOpen: true, _isAlive: true }) return cat1;

   if (cat1._isAlive && cat2._isAlive) return cat1;

   return !cat1._isAlive ? cat1 : cat2;
}

public static SchrodingerCat operator |(SchrodingerCat cat1, SchrodingerCat cat2)
{
   if (!cat1._isBoxOpen) return cat1;

   if (!cat2._isBoxOpen) return cat2;

   if (cat1._isAlive) return cat1;

   if (cat2._isAlive) return cat2;

   return cat1;
} 

Noe the following compiles:

if (cat && !cat) Console.WriteLine("The Cat is both alive and dead!");

And returns true if the box is closed.

The following code:

SchrodingerCat cat = new(1_000_000);
SchrodingerCat cat2 = new(0);

cat.CloseInsideBox();
cat2.CloseInsideBox();

if 
   (cat && !cat) Console.WriteLine("The Cat is both alive and dead!");
else
{
   if(cat) Console.WriteLine("The Cat is alive");
   if(!cat) Console.WriteLine("The Cat is dead");
}

if 
   (cat2 && !cat2) Console.WriteLine("The Cat2 is both alive and dead!");
else
{
   if (cat2) Console.WriteLine("The Cat2 is alive");
   if (!cat2) Console.WriteLine("The Cat2 is dead");
}

if (cat & cat2) Console.WriteLine("& operator returns true");
else Console.WriteLine("& operator returns false");

if (cat | cat2) Console.WriteLine("| operator returns true");
else Console.WriteLine("| operator returns false");

Console.ReadLine();

cat2.OpenBox();

if 
   (cat && !cat) Console.WriteLine("The Cat is both alive and dead!");
else
{
   if(cat) Console.WriteLine("The Cat is alive");
   if(!cat) Console.WriteLine("The Cat is dead");
}

if 
   (cat2 && !cat2) Console.WriteLine("The Cat is both alive and dead!");
else
{
   if (cat2) Console.WriteLine("The Cat2 is alive");
   if (!cat2) Console.WriteLine("The Cat2 is dead");
}

if (cat & cat2) Console.WriteLine("& operator returns true");
else Console.WriteLine("& operator returns false");

if (cat | cat2) Console.WriteLine("| operator returns true");
else Console.WriteLine("| operator returns false");

Will give a result of:

The Cat is both alive and dead!
The Cat2 is both alive and dead!
& operator returns true
| operator returns true

The Cat is both alive and dead!
The Cat2 is dead
& operator returns false
| operator returns true

Which is what we expect and it is the same result as when we had just used the implicit conversion to bool. But there is a difference now for the conditional AND operator.

The Conditional (Short Circuit) And/Or Operators And Their Commutative Property

The conditional or short-circuit operators cannot be overridden. The reason is that these operators don’t exist in isolation but are defined based on the overridden true/false operators and the overridden AND/OR operators of a type. The conditional OR operator || is evaluated like this: T.true(x) ? x : T.|(x, y) and the conditional AND operator && like this: T.false(x) ? x : T.&(x, y).

The usage of the false operator in the conditional AND operator is very important. As I mentioned before, for the boolean logic the true operator and the negation operator are enough. The false operator is used in the short circuit version (&&) of the AND operator (&). Now, if we run the following:

SchrodingerCat cat = new(1_000_000);
SchrodingerCat cat2 = new(0);

cat.CloseInsideBox();
cat2.CloseInsideBox();

if 
   (cat && !cat) Console.WriteLine("The Cat is both alive and dead!");
else
{
   if(cat) Console.WriteLine("The Cat is alive");
   if(!cat) Console.WriteLine("The Cat is dead");
}

if 
   (cat2 && !cat2) Console.WriteLine("The Cat2 is both alive and dead!");
else
{
   if (cat2) Console.WriteLine("The Cat2 is alive");
   if (!cat2) Console.WriteLine("The Cat2 is dead");
}

if (cat & cat2) Console.WriteLine("& operator returns true");
else Console.WriteLine("& operator returns false");

if (cat && cat2) Console.WriteLine("&& operator returns true");
else Console.WriteLine("&& operator returns false");

if (cat2 && cat) Console.WriteLine("&& operator reversed returns true");
else Console.WriteLine("&& operator reversed returns false");

if (cat | cat2) Console.WriteLine("| operator returns true");
else Console.WriteLine("| operator returns false");

if (cat || cat2) Console.WriteLine("|| operator returns true");
else Console.WriteLine("|| operator returns false");

if (cat2 || cat) Console.WriteLine("|| operator reversed returns true");
else Console.WriteLine("|| operator reversed returns false");

Console.ReadLine();

cat2.OpenBox();

if 
   (cat && !cat) Console.WriteLine("The Cat is both alive and dead!");
else
{
   if(cat) Console.WriteLine("The Cat is alive");
   if(!cat) Console.WriteLine("The Cat is dead");
}

if 
   (cat2 && !cat2) Console.WriteLine("The Cat is both alive and dead!");
else
{
   if (cat2) Console.WriteLine("The Cat2 is alive");
   if (!cat2) Console.WriteLine("The Cat2 is dead");
}

if (cat & cat2) Console.WriteLine("& operator returns true");
else Console.WriteLine("& operator returns false");

if (cat && cat2) Console.WriteLine("&& operator returns true");
else Console.WriteLine("&& operator returns false");

if (cat2 && cat) Console.WriteLine("&& operator reversed returns true");
else Console.WriteLine("&& operator reversed returns false");

if (cat | cat2) Console.WriteLine("| operator returns true");
else Console.WriteLine("| operator returns false");

if (cat || cat2) Console.WriteLine("|| operator returns true");
else Console.WriteLine("|| operator returns false");

if (cat2 || cat) Console.WriteLine("|| operator reversed returns true");
else Console.WriteLine("|| operator reversed returns false");

The result will be:

The Cat is both alive and dead!
The Cat2 is both alive and dead!
& operator returns true
&& operator returns true
&& operator reversed returns true
| operator returns true
|| operator returns true
|| operator reversed returns true

The Cat is both alive and dead!
The Cat2 is dead
& operator returns false
&& operator returns true
&& operator reversed returns false
| operator returns true
|| operator returns true
|| operator reversed returns true

Watch the difference between the AND operator and the short-circuit version, as well as the loss of the commutative property for the short-circuit version.

This happens because the SchrodingerCat.false(cat) is true, but the result, which is using the true operator is also true. For a type that can be in a state of both true and false at the same time, an implementation like this will always return true, if an object in that state is the first parameter in a short-circuit AND boolean operation.

This may be desirable, or it may not be. If it is not, then we can change the overridden AND operator or the overridden false operator implementations to what is defined by our requirements, but this serves as an example that there is a difference between overriding the implicit implementation to bool and overriding the true/false and the AND/OR boolean operators.

In the first case we can only have boolean logic between booleans. In the second case, we are not required to have boolean logic, but we can have operations in our type that will eventually return a true or false literal value by operations that follow the logic of our requirements.

Conclusion

This was a more in depth example of how to use the boolean logical operators in our types. For 99.9% of the cases, using a nullable boolean type or an implicit conversion to bool is enough. For other cases, combining the implicit conversion with the override of the negation operator (!) will do. But there is a fraction of the percentage for cases that the implementation of the true/false operators, combined with the implementation of the AND/OR operators will allow us to create logical operations between objects of our type that give a true/false result without following boolean logic.

This was not a post that will not be very useful in every day programming, but I had fun exploring the ways of implementing a type that can have a state that is considered both true and false at the same time, and how the operations of AND,OR and Negation can give results when used with that type, and thought I should share my findings.

Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.


Follow me: