Checked user-defined operators

Note

This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.

There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinent language design meeting (LDM) notes.

You can learn more about the process for adopting feature speclets into the C# language standard in the article on the specifications.

Summary

C# should support defining checked variants of the following user-defined operators so that users can opt into or out of overflow behavior as appropriate:

  • The ++ and -- unary operators §11.7.14 and §11.8.6.
  • The - unary operator §11.8.3.
  • The +, -, *, and / binary operators §11.9.
  • Explicit conversion operators.

Motivation

There is no way for a user to declare a type and support both checked and unchecked versions of an operator. This will make it hard to port various algorithms to use the proposed generic math interfaces exposed by the libraries team. Likewise, this makes it impossible to expose a type such as Int128 or UInt128 without the language simultaneously shipping its own support to avoid breaking changes.

Detailed design

Syntax

Grammar at operators (§14.10) will be adjusted to allow checked keyword after the operator keyword right before the operator token:

overloadable_unary_operator
    : '+' | 'checked'? '-' | '!' | '~' | 'checked'? '++' | 'checked'? '--' | 'true' | 'false'
    ;

overloadable_binary_operator
    : 'checked'? '+'   | 'checked'? '-'   | 'checked'? '*'   | 'checked'? '/'   | '%'   | '&'   | '|'   | '^'   | '<<'
    | right_shift | '=='  | '!='  | '>'   | '<'   | '>='  | '<='
    ;
    
conversion_operator_declarator
    : 'implicit' 'operator' type '(' type identifier ')'
    | 'explicit' 'operator' 'checked'? type '(' type identifier ')'
    ;    

For example:

public static T operator checked ++(T x) {...}
public static T operator checked --(T x) {...}
public static T operator checked -(T x) {...}
public static T operator checked +(T lhs, T rhs) {...}
public static T operator checked -(T lhs, T rhs) {...}
public static T operator checked *(T lhs, T rhs) {...}
public static T operator checked /(T lhs, T rhs) {...}
public static explicit operator checked U(T x) {...}
public static T I1.operator checked ++(T x) {...}
public static T I1.operator checked --(T x) {...}
public static T I1.operator checked -(T x) {...}
public static T I1.operator checked +(T lhs, T rhs) {...}
public static T I1.operator checked -(T lhs, T rhs) {...}
public static T I1.operator checked *(T lhs, T rhs) {...}
public static T I1.operator checked /(T lhs, T rhs) {...}
public static explicit I1.operator checked U(T x) {...}

For brevity below, an operator with the checked keyword is referred to as a checked operator and an operator without it is referred to as a regular operator. These terms are not applicable to operators that don't have a checked form.

Semantics

A user-defined checked operator is expected to throw an exception when the result of an operation is too large to represent in the destination type. What does it mean to be too large actually depends on the nature of the destination type and is not prescribed by the language. Typically the exception thrown is a System.OverflowException, but the language doesn't have any specific requirements regarding this.

A user-defined regular operator is expected to not throw an exception when the result of an operation is too large to represent in the destination type. Instead, it is expected to return an instance representing a truncated result. What does it mean to be too large and to be truncated actually depends on the nature of the destination type and is not prescribed by the language.

All existing user-defined operators out there that will have checked form supported fall into the category of regular operators. It is understood that many of them are likely to not follow the semantics specified above, but for the purpose of semantic analysis, compiler will assume that they are.

Checked vs. unchecked context within a checked operator

Checked/unchecked context within the body of a checked operator is not affected by the presence of the checked keyword. In other words, the context is the same as immediately at the beginning of the operator declaration. The developer would need to explicitly switch the context if part of their algorithm cannot rely on default context.

Names in metadata

Section "I.10.3.1 Unary operators" of ECMA-335 will be adjusted to include op_CheckedIncrement, op_CheckedDecrement, op_CheckedUnaryNegation as the names for methods implementing checked ++, -- and - unary operators.

Section "I.10.3.2 Binary operators" of ECMA-335 will be adjusted to include op_CheckedAddition, op_CheckedSubtraction, op_CheckedMultiply, op_CheckedDivision as the names for methods implementing checked +, -, *, and / binary operators.

Section "I.10.3.3 Conversion operators" of ECMA-335 will be adjusted to include op_CheckedExplicit as the name for a method implementing checked explicit conversion operator.

Unary operators

Unary checked operators follow the rules from §14.10.2.

Also, a checked operator declaration requires a pair-wise declaration of a regular operator (the return type should match as well). A compile-time error occurs otherwise.

public struct Int128
{
    // This is fine, both a checked and regular operator are defined
    public static Int128 operator checked -(Int128 lhs);
    public static Int128 operator -(Int128 lhs);

    // This is fine, only a regular operator is defined
    public static Int128 operator --(Int128 lhs);

    // This should error, a regular operator must also be defined
    public static Int128 operator checked ++(Int128 lhs);
}

Binary operators

Binary checked operators follow the rules from §14.10.3.

Also, a checked operator declaration requires a pair-wise declaration of a regular operator (the return type should match as well). A compile-time error occurs otherwise.

public struct Int128
{
    // This is fine, both a checked and regular operator are defined
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    // This is fine, only a regular operator is defined
    public static Int128 operator -(Int128 lhs, Int128 rhs);

    // This should error, a regular operator must also be defined
    public static Int128 operator checked *(Int128 lhs, Int128 rhs);
}

Candidate user-defined operators

The https://github.com/dotnet/csharplang/blob/main/spec/expressions.md#candidate-user-defined-operators section will be adjusted as follows (additions/changes are in bold).

Given a type T and an operation operator op(A), where op is an overloadable operator and A is an argument list, the set of candidate user-defined operators provided by T for operator op(A) is determined as follows:

  • Determine the type T0. If T is a nullable type, T0 is its underlying type, otherwise T0 is equal to T.
  • Find the set of user-defined operators, U. This set consists of:
    • In unchecked evaluation context, all regular operator op declarations in T0.
    • In checked evaluation context, all checked and regular operator op declarations in T0 except regular declarations that have pair-wise matching checked operator declaration.
  • For all operator op declarations in U and all lifted forms of such operators, if at least one operator is applicable (Applicable function member) with respect to the argument list A, then the set of candidate operators consists of all such applicable operators in T0.
  • Otherwise, if T0 is object, the set of candidate operators is empty.
  • Otherwise, the set of candidate operators provided by T0 is the set of candidate operators provided by the direct base class of T0, or the effective base class of T0 if T0 is a type parameter.

Similar rules will be applied while determining the set of candidate operators in interfaces https://github.com/dotnet/csharplang/blob/main/meetings/2017/LDM-2017-06-27.md#shadowing-within-interfaces.

The section §11.7.18 will be adjusted to reflect the effect that the checked/unchecked context has on unary and binary operator overload resolution.

Example #1:

public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedAddition`
        Int128 r1 = checked(lhs + rhs);

        // Resolves to `op_Addition`
        Int128 r2 = unchecked(lhs + rhs);

        // Resolve to `op_Subtraction`
        Int128 r3 = checked(lhs - rhs);

        // Resolve to `op_Subtraction`
        Int128 r4 = unchecked(lhs - rhs);

        // Resolves to `op_CheckedMultiply`
        Int128 r5 = checked(lhs * rhs);

        // Error: Operator '*' cannot be applied to operands of type 'Int128' and 'Int128'
        Int128 r6 = unchecked(lhs * rhs);
    }

    public static void Divide(Int128 lhs, byte rhs)
    {
        // Resolves to `op_Division` - it is a better match than `op_CheckedDivision`
        Int128 r4 = checked(lhs / rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    public static Int128 operator -(Int128 lhs, Int128 rhs);

    // Cannot be declared in C#, but could be declared by some other language
    public static Int128 operator checked *(Int128 lhs, Int128 rhs);

    // Cannot be declared in C#, but could be declared by some other language
    public static Int128 operator checked /(Int128 lhs, int rhs);

    public static Int128 operator /(Int128 lhs, byte rhs);
}

Example #2:

class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // error CS0034: Operator '+' is ambiguous on operands of type 'C2' and 'C3'
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    // Cannot be declared in C#, but could be declared by some other language
    public static C1 operator checked + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}

Example #3:

class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // error CS0034: Operator '+' is ambiguous on operands of type 'C2' and 'C3'
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    // Cannot be declared in C#, but could be declared by some other language
    public static C2 operator checked + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}

Conversion operators

Conversion checked operators follow the rules from §14.10.4.

However, a checked operator declaration requires a pair-wise declaration of a regular operator. A compile-time error occurs otherwise.

The following paragraph

The signature of a conversion operator consists of the source type and the target type. (This is the only form of member for which the return type participates in the signature.) The implicit or explicit classification of a conversion operator is not part of the operator's signature. Thus, a class or struct cannot declare both an implicit and an explicit conversion operator with the same source and target types.

will be adjusted to allow a type to declare checked and regular forms of explicit conversions with the same source and target types. A type will not be allowed to declare both an implicit and a checked explicit conversion operator with the same source and target types.

Processing of user-defined explicit conversions

The third bullet in §10.5.5:

  • Find the set of applicable user-defined and lifted conversion operators, U. This set consists of the user-defined and lifted implicit or explicit conversion operators declared by the classes or structs in D that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.

will be replaced with the following bullet points:

  • Find the set of conversion operators, U0. This set consists of:
    • In unchecked evaluation context, the user-defined implicit or regular explicit conversion operators declared by the classes or structs in D.
    • In checked evaluation context, the user-defined implicit or regular/checked explicit conversion operators declared by the classes or structs in D except regular explicit conversion operators that have pair-wise matching checked operator declaration within the same declaring type.
  • Find the set of applicable user-defined and lifted conversion operators, U. This set consists of the user-defined and lifted implicit or explicit conversion operators in U0 that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.

The Checked and unchecked operators §11.7.18 section will be adjusted to reflect the effect that the checked/unchecked context has on processing of user-defined explicit conversions.

Implementing operators

A checked operator does not implement a regular operator and vice versa.

Linq Expression Trees

Checked operators will be supported in Linq Expression Trees. A UnaryExpression/BinaryExpression node will be created with corresponding MethodInfo. The following factory methods will be used:

public static UnaryExpression NegateChecked (Expression expression, MethodInfo? method);

public static BinaryExpression AddChecked (Expression left, Expression right, MethodInfo? method);
public static BinaryExpression SubtractChecked (Expression left, Expression right, MethodInfo? method);
public static BinaryExpression MultiplyChecked (Expression left, Expression right, MethodInfo? method);

public static UnaryExpression ConvertChecked (Expression expression, Type type, MethodInfo? method);

Note, that C# doesn't support assignments in expression trees, therefore checked increment/decrement will not be supported as well.

There is no factory method for checked divide. There is an open question regarding this - Checked division in Linq Expression Trees.

Dynamic

We will investigate the cost of adding support for checked operators in dynamic invocation in CoreCLR and pursue an implementation if the cost is not too high. This is a quote from https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-09.md.

Drawbacks

This adds additional complexity to the language and allows users to introduce more kinds of breaking changes to their types.

Alternatives

The generic math interfaces that the libraries plans to expose could expose named methods (such as AddChecked). The primary drawback is that this is less readable/maintainable and doesn't get the benefit of the language precedence rules around operators.

Placement of the checked keyword

Alternatively the checked keyword could be moved to the place right before the operator keyword:

public static T checked operator ++(T x) {...}
public static T checked operator --(T x) {...}
public static T checked operator -(T x) {...}
public static T checked operator +(T lhs, T rhs) {...}
public static T checked operator -(T lhs, T rhs) {...}
public static T checked operator *(T lhs, T rhs) {...}
public static T checked operator /(T lhs, T rhs) {...}
public static explicit checked operator U(T x) {...}
public static T checked I1.operator ++(T x) {...}
public static T checked I1.operator --(T x) {...}
public static T checked I1.operator -(T x) {...}
public static T checked I1.operator +(T lhs, T rhs) {...}
public static T checked I1.operator -(T lhs, T rhs) {...}
public static T checked I1.operator *(T lhs, T rhs) {...}
public static T checked I1.operator /(T lhs, T rhs) {...}
public static explicit checked I1.operator U(T x) {...}

Or it could be moved into the set of operator modifiers:

operator_modifier
    : 'public'
    | 'static'
    | 'extern'
    | 'checked'
    | operator_modifier_unsafe
    ;
public static checked T operator ++(T x) {...}
public static checked T operator --(T x) {...}
public static checked T operator -(T x) {...}
public static checked T operator +(T lhs, T rhs) {...}
public static checked T operator -(T lhs, T rhs) {...}
public static checked T operator *(T lhs, T rhs) {...}
public static checked T operator /(T lhs, T rhs) {...}
public static checked explicit operator U(T x) {...}
public static checked T I1.operator ++(T x) {...}
public static checked T I1.operator --(T x) {...}
public static checked T I1.operator -(T x) {...}
public static checked T I1.operator +(T lhs, T rhs) {...}
public static checked T I1.operator -(T lhs, T rhs) {...}
public static checked T I1.operator *(T lhs, T rhs) {...}
public static checked T I1.operator /(T lhs, T rhs) {...}
public static checked explicit I1.operator U(T x) {...}

unchecked keyword

There were suggestions to support unchecked keyword at the same position as the checked keyword with the following possible meanings:

  • Simply to explicitly reflect the regular nature of the operator, or
  • Perhaps to designate a distinct flavor of an operator that is supposed to be used in an unchecked context. The language could support op_Addition, op_CheckedAddition, and op_UncheckedAddition to help limit the number of breaking changes. This adds another layer of complexity that is likely not necessary in most code.

Operator names in ECMA-335

Alternatively the operator names could be op_UnaryNegationChecked, op_AdditionChecked, op_SubtractionChecked, op_MultiplyChecked, op_DivisionChecked, with Checked at the end. However, it looks like there is already a pattern established to end the names with the operator word. For example, there is a op_UnsignedRightShift operator rather than op_RightShiftUnsigned operator.

Checked operators are inapplicable in an unchecked context

The compiler, when performing member lookup to find candidate user-defined operators within an unchecked context, could ignore checked operators. If metadata is encountered that only defines a checked operator, then a compilation error will occur.

public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedMultiply`
        Int128 r5 = checked(lhs * rhs);

        // Error: Operator '*' cannot be applied to operands of type 'Int128' and 'Int128'
        Int128 r5 = unchecked(lhs * rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked *(Int128 lhs, Int128 rhs);
}

More complicated operator lookup and overload resolution rules in a checked context

The compiler, when performing member lookup to find candidate user-defined operators within a checked context will also consider applicable operators ending with Checked. That is, if the compiler was attempting to find applicable function members for the binary addition operator, it would look for both op_Addition and op_AdditionChecked. If the only applicable function member is a checked operator, it will be used. If both a regular operator and checked operator exist and are equally applicable the checked operator will be preferred. If both a regular operator and a checked operator exist but the regular operator is an exact match while the checked operator is not, the compiler will prefer the regular operator.

public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedAddition`
        Int128 r1 = checked(lhs + rhs);

        // Resolves to `op_Addition`
        Int128 r2 = unchecked(lhs + rhs);

        // Resolve to `op_Subtraction`
        Int128 r3 = checked(lhs - rhs);

        // Resolve to `op_Subtraction`
        Int128 r4 = unchecked(lhs - rhs);
    }

    public static void Multiply(Int128 lhs, byte rhs)
    {
        // Resolves to `op_Multiply` even though `op_CheckedMultiply` is also applicable
        Int128 r4 = checked(lhs * rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    public static Int128 operator -(Int128 lhs, Int128 rhs);

    public static Int128 operator checked *(Int128 lhs, int rhs);
    public static Int128 operator *(Int128 lhs, byte rhs);
}

Yet another way to build the set of candidate user-defined operators

Unary operator overload resolution

Assuming that regular operator matches unchecked evaluation context, checked operator matches checked evaluation context and an operator that doesn't have checked form (for example, +) matches either context, the first bullet in https://github.com/dotnet/csharplang/blob/main/spec/expressions.md#unary-operator-overload-resolution:

will be replaced with the following two bullet points:

  • The set of candidate user-defined operators provided by X for the operation operator op(x) matching the current checked/unchecked context is determined using the rules of Candidate user-defined operators.
  • If the set of candidate user-defined operators is not empty, then this becomes the set of candidate operators for the operation. Otherwise, the set of candidate user-defined operators provided by X for the operation operator op(x) matching the opposite checked/unchecked context is determined using the rules of Candidate user-defined operators.

Binary operator overload resolution

Assuming that regular operator matches unchecked evaluation context, checked operator matches checked evaluation context and an operator that doesn't have a checked form (for example, %) matches either context, the first bullet in https://github.com/dotnet/csharplang/blob/main/spec/expressions.md#binary-operator-overload-resolution:

  • The set of candidate user-defined operators provided by X and Y for the operation operator op(x,y) is determined. The set consists of the union of the candidate operators provided by X and the candidate operators provided by Y, each determined using the rules of Candidate user-defined operators. If X and Y are the same type, or if X and Y are derived from a common base type, then shared candidate operators only occur in the combined set once.

will be replaced with the following two bullet points:

  • The set of candidate user-defined operators provided by X and Y for the operation operator op(x,y) matching the current checked/unchecked context is determined. The set consists of the union of the candidate operators provided by X and the candidate operators provided by Y, each determined using the rules of Candidate user-defined operators. If X and Y are the same type, or if X and Y are derived from a common base type, then shared candidate operators only occur in the combined set once.
  • If the set of candidate user-defined operators is not empty, then this becomes the set of candidate operators for the operation. Otherwise, the set of candidate user-defined operators provided by X and Y for the operation operator op(x,y) matching the opposite checked/unchecked context is determined. The set consists of the union of the candidate operators provided by X and the candidate operators provided by Y, each determined using the rules of Candidate user-defined operators. If X and Y are the same type, or if X and Y are derived from a common base type, then shared candidate operators only occur in the combined set once.
Example #1:
public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedAddition`
        Int128 r1 = checked(lhs + rhs);

        // Resolves to `op_Addition`
        Int128 r2 = unchecked(lhs + rhs);

        // Resolve to `op_Subtraction`
        Int128 r3 = checked(lhs - rhs);

        // Resolve to `op_Subtraction`
        Int128 r4 = unchecked(lhs - rhs);

        // Resolves to `op_CheckedMultiply`
        Int128 r5 = checked(lhs * rhs);

        // Resolves to `op_CheckedMultiply`
        Int128 r5 = unchecked(lhs * rhs);
    }

    public static void Divide(Int128 lhs, byte rhs)
    {
        // Resolves to `op_CheckedDivision`
        Int128 r4 = checked(lhs / rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    public static Int128 operator -(Int128 lhs, Int128 rhs);

    public static Int128 operator checked *(Int128 lhs, Int128 rhs);

    public static Int128 operator checked /(Int128 lhs, int rhs);
    public static Int128 operator /(Int128 lhs, byte rhs);
}
Example #2:
class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // C1.op_CheckedAddition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator checked + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}
Example #3:
class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // C2.op_CheckedAddition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator checked + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}
Example #4:
class C
{
    static void Add(C2 x, byte y)
    {
        object o;
        
        // C1.op_CheckedAddition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }

    static void Add2(C2 x, int y)
    {
        object o;
        
        // C2.op_Addition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator checked + (C1 x, byte y) => new C1();
}

class C2 : C1
{
    public static C2 operator + (C2 x, int y) => new C2();
}
Example #5:
class C
{
    static void Add(C2 x, byte y)
    {
        object o;
        
        // C2.op_CheckedAddition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }

    static void Add2(C2 x, int y)
    {
        object o;
        
        // C1.op_Addition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, int y) => new C1();
}

class C2 : C1
{
    public static C2 operator checked + (C2 x, byte y) => new C2();
}

Processing of user-defined explicit conversions

Assuming that regular operator matches unchecked evaluation context and checked operator matches checked evaluation context, the third bullet in https://github.com/dotnet/csharplang/blob/main/spec/conversions.md#processing-of-user-defined-explicit-conversions:

  • Find the set of applicable user-defined and lifted conversion operators, U. This set consists of the user-defined and lifted implicit or explicit conversion operators declared by the classes or structs in D that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.

will be replaced with the following bullet points:

  • Find the set of applicable user-defined and lifted explicit conversion operators matching the current checked/unchecked context, U0. This set consists of the user-defined and lifted explicit conversion operators declared by the classes or structs in D that match the current checked/unchecked context and convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T.
  • Find the set of applicable user-defined and lifted explicit conversion operators matching the opposite checked/unchecked context, U1. If U0 is not empty, U1 is empty. Otherwise, this set consists of the user-defined and lifted explicit conversion operators declared by the classes or structs in D that match the opposite checked/unchecked context and convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T.
  • Find the set of applicable user-defined and lifted conversion operators, U. This set consists of operators from U0, U1, and the user-defined and lifted implicit conversion operators declared by the classes or structs in D that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.

Yet another another way to build the set of candidate user-defined operators

Unary operator overload resolution

The first bullet in section §11.4.4 will be adjusted as follows (additions are in bold).

  • The set of candidate user-defined operators provided by X for the operation operator op(x) is determined using the rules of "Candidate user-defined operators" section below. If the set contains at least one operator in checked form, all operators in regular form are removed from the set.

The section §11.7.18 will be adjusted to reflect the effect that the checked/unchecked context has on unary operator overload resolution.

Binary operator overload resolution

The first bullet in section §11.4.5 will be adjusted as follows (additions are in bold).

  • The set of candidate user-defined operators provided by X and Y for the operation operator op(x,y) is determined. The set consists of the union of the candidate operators provided by X and the candidate operators provided by Y, each determined using the rules of "Candidate user-defined operators" section below. If X and Y are the same type, or if X and Y are derived from a common base type, then shared candidate operators only occur in the combined set once. If the set contains at least one operator in checked form, all operators in regular form are removed from the set.

The Checked and unchecked operators §11.7.18 section will be adjusted to reflect the effect that the checked/unchecked context has on binary operator overload resolution.

Candidate user-defined operators

The https://github.com/dotnet/csharplang/blob/main/spec/expressions.md#candidate-user-defined-operators section will be adjusted as follows (additions are in bold).

Given a type T and an operation operator op(A), where op is an overloadable operator and A is an argument list, the set of candidate user-defined operators provided by T for operator op(A) is determined as follows:

  • Determine the type T0. If T is a nullable type, T0 is its underlying type, otherwise T0 is equal to T.
  • For all operator op declarations in their checked and regular forms in checked evaluation context and only in their regular form in unchecked evaluation context in T0 and all lifted forms of such operators, if at least one operator is applicable (Applicable function member) with respect to the argument list A, then the set of candidate operators consists of all such applicable operators in T0.
  • Otherwise, if T0 is object, the set of candidate operators is empty.
  • Otherwise, the set of candidate operators provided by T0 is the set of candidate operators provided by the direct base class of T0, or the effective base class of T0 if T0 is a type parameter.

Similar filtering will be applied while determining the set of candidate operators in interfaces https://github.com/dotnet/csharplang/blob/main/meetings/2017/LDM-2017-06-27.md#shadowing-within-interfaces.

The https://github.com/dotnet/csharplang/blob/main/spec/expressions.md#the-checked-and-unchecked-operators section will be adjusted to reflect the effect that the checked/unchecked context has on unary and binary operator overload resolution.

Example #1:
public class MyClass
{
    public static void Add(Int128 lhs, Int128 rhs)
    {
        // Resolves to `op_CheckedAddition`
        Int128 r1 = checked(lhs + rhs);

        // Resolves to `op_Addition`
        Int128 r2 = unchecked(lhs + rhs);

        // Resolve to `op_Subtraction`
        Int128 r3 = checked(lhs - rhs);

        // Resolve to `op_Subtraction`
        Int128 r4 = unchecked(lhs - rhs);

        // Resolves to `op_CheckedMultiply`
        Int128 r5 = checked(lhs * rhs);

        // Error: Operator '*' cannot be applied to operands of type 'Int128' and 'Int128'
        Int128 r5 = unchecked(lhs * rhs);
    }

    public static void Divide(Int128 lhs, byte rhs)
    {
        // Resolves to `op_CheckedDivision`
        Int128 r4 = checked(lhs / rhs);
    }
}

public struct Int128
{
    public static Int128 operator checked +(Int128 lhs, Int128 rhs);
    public static Int128 operator +(Int128 lhs, Int128 rhs);

    public static Int128 operator -(Int128 lhs, Int128 rhs);

    public static Int128 operator checked *(Int128 lhs, Int128 rhs);

    public static Int128 operator checked /(Int128 lhs, int rhs);
    public static Int128 operator /(Int128 lhs, byte rhs);
}
Example #2:
class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // C1.op_CheckedAddition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator checked + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}
Example #3:
class C
{
    static void Add(C2 x, C3 y)
    {
        object o;
        
        // C2.op_CheckedAddition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, C3 y) => new C3();
}

class C2 : C1
{
    public static C2 operator checked + (C2 x, C1 y) => new C2();
}

class C3 : C1
{
}
Example #4:
class C
{
    static void Add(C2 x, byte y)
    {
        object o;
        
        // C2.op_Addition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }

    static void Add2(C2 x, int y)
    {
        object o;
        
        // C2.op_Addition
        o = checked(x + y);
        
        // C2.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator checked + (C1 x, byte y) => new C1();
}

class C2 : C1
{
    public static C2 operator + (C2 x, int y) => new C2();
}
Example #5:
class C
{
    static void Add(C2 x, byte y)
    {
        object o;
        
        // C2.op_CheckedAddition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }

    static void Add2(C2 x, int y)
    {
        object o;
        
        // C1.op_Addition
        o = checked(x + y);
        
        // C1.op_Addition
        o = unchecked(x + y);
    }
}

class C1
{
    public static C1 operator + (C1 x, int y) => new C1();
}

class C2 : C1
{
    public static C2 operator checked + (C2 x, byte y) => new C2();
}

Processing of user-defined explicit conversions

The third bullet in §10.5.5:

  • Find the set of applicable user-defined and lifted conversion operators, U. This set consists of the user-defined and lifted implicit or explicit conversion operators declared by the classes or structs in D that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.

will be replaced with the following bullet points:

  • Find the set of applicable user-defined and lifted explicit conversion operators, U0. This set consists of the user-defined and lifted explicit conversion operators declared by the classes or structs in D in their checked and regular forms in checked evaluation context and only in their regular form in unchecked evaluation context and convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T.
  • If U0 contains at least one operator in checked form, all operators in regular form are removed from the set.
  • Find the set of applicable user-defined and lifted conversion operators, U. This set consists of operators from U0, and the user-defined and lifted implicit conversion operators declared by the classes or structs in D that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.

The Checked and unchecked operators §11.7.18 section will be adjusted to reflect the effect that the checked/unchecked context has on processing of user-defined explicit conversions.

Checked vs. unchecked context within a checked operator

The compiler could treat the default context of a checked operator as checked. The developer would need to explicitly use unchecked if part of their algorithm should not participate in the checked context. However, this might not work well in the future if we start allowing checked/unchecked tokens as modifiers on operators to set the context within the body. The modifier and the keyword could contradict each other. Also, we wouldn't be able to do the same (treat default context as unchecked) for a regular operator because that would be a breaking change.

Unresolved questions

Should the language allow checked and unchecked modifiers on methods (e.g. static checked void M())? This would allow removing nesting levels for methods that require it.

Checked division in Linq Expression Trees

There is no factory method to create a checked division node and there is no ExpressionType.DivideChecked member. We could still use the following factory method to create regular divide node with MethodInfo pointing to the op_CheckedDivision method. Consumers will have to check the name to infer the context.

public static BinaryExpression Divide (Expression left, Expression right, MethodInfo? method);

Note, even though §11.7.18 section lists / operator as one of the operators affected by checked/unchecked evaluation context, IL doesn't have a special op code to perform checked division. Compiler always uses the factory method reardless of the context today.

Proposal: Checked user-defined devision will not be supported in Linq Expression Trees.

(Resolved) Should we support implicit checked conversion operators?

In general, implicit conversion operators are not supposed to throw.

Proposal: No.

Resolution: Approved - https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-07.md#checked-implicit-conversions

Design meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-07.md https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-09.md https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-14.md https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-23.md