Operator overloading
Define how a struct participates in expressions and converts to or from other types. This chapter lists every supported operator declaration and explains exactly which operations remain controlled by the language.
What can be overloaded
Xenon supports four unary forms, sixteen binary forms and two conversion forms. These are the complete sets. A token appearing elsewhere in the language is not automatically a valid operator declaration.
The number of parameters distinguishes unary and binary + and -. The spellings * and & are overloadable only as binary operators; their unary dereference and address-of forms cannot be overloaded.
+= and the other compound assignments can use a user-defined binary operator, but cannot have their own operator += declarations. See the complete mapping below.
Unary operators: exactly one parameter
Each row requires one explicit operand parameter. There is no implicit this argument because every operator is static.
| Token | Expression | Conventional meaning | Declaration header inside S |
|---|---|---|---|
+ | +value | Unary plus | public static S operator +(readonly S& value) |
- | -value | Negation | public static S operator -(readonly S& value) |
! | !value | Logical negation | public static bool operator !(readonly S& value) |
~ | ~value | Bitwise complement | public static S operator ~(readonly S& value) |
These headers illustrate conventional return types and readonly-reference operands; they are not mandatory signatures. The operand must be the declaring struct, by value or by reference. Return types follow the ordinary operator rules.
Binary operators: exactly two parameters
The first parameter receives the left operand and the second receives the right operand. At least one of those parameters must have the declaring struct's value type. The other operand can have a different type.
| Token | Expression | Conventional meaning |
|---|---|---|
+ | left + right | Addition or concatenation |
- | left - right | Subtraction |
* | left * right | Multiplication |
/ | left / right | Division |
% | left % right | Remainder |
& | left & right | Bitwise AND |
| | left | right | Bitwise OR |
^ | left ^ right | Bitwise XOR |
<< | left << right | Left shift |
>> | left >> right | Right shift |
== | left == right | Equality |
!= | left != right | Inequality |
< | left < right | Less than |
> | left > right | Greater than |
<= | left <= right | Less than or equal |
>= | left >= right | Greater than or equal |
For example, a struct S can declare public static S operator +(readonly S& left, int right). That declaration handles s + 1. It does not by itself handle 1 + s; supporting that order requires an applicable declaration with the operands in that order.
The shift operators have the same two-parameter rule as other binary operators. User-defined shifts do not impose a special mandatory int type on the second parameter. An operator body supplies the meaning; the compiler does not check arithmetic laws such as commutativity or associativity.
Declaration and signature rules
// Declaration shape; ReturnType, token and ParameterList are placeholders.
public static ReturnType operator token(ParameterList)
{
// Ordinary function body.
}
- Declare the operator inside a struct. It must be
static; namespace-level operator functions and instance operators are not supported. - Use a supported token and the exact parameter count listed above. Unary and binary overloads are separate declarations.
- For an ordinary operator, at least one operand must be the declaring struct:
S,S&orreadonly S&. Merely returningSis insufficient. A parameter of typeS*alone does not meet this requirement. - The other binary operand and the return value need not be the same type as the declaring struct. For example,
S + intcan returnS, whileS - Scan returnint. - Ordinary operators follow ordinary return-type and reference-return lifetime rules. The compiler does not force comparison operators or
!to returnbool, although that is their conventional API. The expression's surrounding context still checks the result type. - Choose the access modifier for the intended callers.
publicis useful for an operator exposed as part of a type's API; private and other restricted members participate only where accessible. virtual,overrideandabstractoperator declarations are rejected. Operators are resolved as static calls.- A readonly effect contract uses the normal callable syntax, for example
public static S readonly operator +(readonly S& a, int b). The readonly rules still apply to its body.
Parameter names do not distinguish overloads. Ordinary operators cannot be overloaded solely by return type. Conversion operators have additional source/destination identity rules described below.
Overloading does not change token precedence, associativity, grouping or short-circuit syntax. For example, a + b * c still groups as a + (b * c). Declaring an operator does not create a new infix token or make two unrelated primitive operands discover that struct's operators.
Complete arithmetic and equality example
This type accepts integers through an implicit conversion, defines arithmetic and equality, and exposes an explicit conversion back to int.
namespace OperatorExamples;
struct Number
{
public int Value;
public static Number operator implicit(int value)
{
return Number { value };
}
public static int operator explicit(readonly Number& value)
{
return value.Value;
}
public static Number operator +(readonly Number& left, readonly Number& right)
{
return Number { left.Value + right.Value };
}
public static Number operator -(readonly Number& value)
{
return Number { -value.Value };
}
public static bool operator ==(readonly Number& left, readonly Number& right)
{
return left.Value == right.Value;
}
public static bool operator !=(readonly Number& left, readonly Number& right)
{
return !(left == right);
}
}
int Demo()
{
Number first = 10;
Number second = 20;
Number sum = first + second;
sum += 2;
Number negative = -sum;
if (negative != -32) return 0;
return cast<int>(sum); // 32
}
The 2 in sum += 2 becomes a Number because the selected binary + expects one. There is no separate compound-assignment operator body. The != body explicitly invokes ==; this relationship is written by the library author.
Candidate lookup and overload resolution
- The compiler looks for the requested operator kind in the operand structs and their inherited members. It does not search arbitrary unrelated structs.
- Accessibility is checked at the expression's use site, before inaccessible members can hide inherited candidates or participate in ranking.
- Applicable candidates are compared using ordinary callable overload resolution. For each argument, an exact match is preferred to a standard conversion, and a standard conversion is preferred to a user-defined implicit conversion.
- If one applicable user operator wins, its body is called using ordinary argument-passing rules.
- If accessible user candidates are ambiguous, compilation fails. If no accessible user operator applies, the compiler tries the existing built-in operator rules; those rules may also reject the expression.
With several arguments, one candidate must be no worse for every argument and better for at least one to win by conversion quality. A candidate that is better only for the left operand does not automatically beat one that is better only for the right operand.
Once a user operator is selected, its return type is checked by the surrounding context. The expected result type does not manufacture an otherwise missing ordinary operator overload.
A private operator == used from outside its permitted scope does not suppress valid built-in struct equality. A private derived operator also cannot hide an otherwise applicable public base operator at that use site.
Custom equality and built-in fallback
An applicable user-defined == takes precedence over the default field-by-field struct comparison for that expression. The same rule applies independently to !=. Without an applicable custom operator, built-in struct equality is available only when the existing field/type rules permit it.
No related operators are synthesized. Declaring == does not generate !=, and declaring < does not generate >, <= or >=. If custom equality intentionally differs from field equality, define both == and != to keep their behavior consistent.
A custom operator does not alter the equality used by atomic compare-exchange. Atomic matching continues to use built-in equality.
Compound assignment: derived from the binary operator
| Expression | Binary operator resolved | Declare this token |
|---|---|---|
target += value | target + value | operator + |
target -= value | target - value | operator - with two parameters |
target *= value | target * value | operator * |
target /= value | target / value | operator / |
target %= value | target % value | operator % |
target &= value | target & value | operator & |
target |= value | target | value | operator | |
target ^= value | target ^ value | operator ^ |
target <<= value | target << value | operator << |
target >>= value | target >> value | operator >> |
The target must be writable and the binary result must be assignable to its type under ordinary assignment rules. A usable + does not guarantee usable +=: a return type that cannot be assigned back makes the compound expression invalid.
The compiler evaluates the left-hand storage location once, reads its value, computes the binary result and assigns it back to that location. In values[GetIndex()] += delta, GetIndex() is evaluated once. Properties and indexers use their normal getter/setter protocol: the receiver and index arguments are evaluated once, the getter obtains the old value, and the setter receives the result. Both accessor operations must be available and valid.
For a destructible storage target, normal replacement destroys its previous value after the replacement result has been produced successfully. If the operator throws, the assignment does not commit a replacement. Side effects already performed by operand evaluation or the operator are not rolled back. A by-value property getter result is a temporary and follows ordinary call/temporary cleanup rules.
Do not model this as textual duplication of the left-hand expression. GetContainer().Item += delta must not become two calls to GetContainer().
Operations that cannot have operator declarations
The following operations are outside user-defined operator declarations. Some have their own language features, linked in the table.
| Token or operation | Rule |
|---|---|
= | Assignment and replacement remain compiler-controlled. An implicit conversion may adapt the assigned value; it does not overload assignment. |
+= -= *= /= %= &= |= ^= <<= >>= | No separate declarations. Use the corresponding binary operator from the table above. |
&& || | Short-circuit operations retain their built-in evaluation rules. Overloading & or | does not overload these tokens. |
++ -- | Neither prefix nor postfix increment/decrement is overloadable. Declaring + or - does not enable them for a struct. |
Unary &, unary * | Address-of and dereference remain language-defined. Only the two-operand forms of these spellings are overloadable. |
[] | Use indexer declarations, not operator []. |
() | Function invocation cannot be customized with operator (). |
., -> | Member access and pointer-member access cannot be overloaded. |
Conditional expressions, including ?: | No conditional operator declaration is supported. |
new, malloc, calloc, free, delete, destruction | Allocation and lifetime operations retain their language rules. Use normal constructors and destructors for object initialization and cleanup. |
move | Ownership transfer follows move semantics; it cannot be replaced by an operator body. |
<-> | Swap/atomic exchange is compiler-controlled. |
target : expected --> desired | Atomic compare-exchange cannot be customized by operators, including custom equality. |
| Other language intrinsics or invented tokens | Only the unary, binary and conversion forms explicitly listed in this chapter are accepted as operator declarations. |
Implicit and explicit conversion operators
A conversion is a static operator with exactly one source parameter. Its return type is the destination. Declare it in either the source struct or the destination struct; the source and destination value types must differ.
| Kind | Declaration shape | Where it can be selected |
|---|---|---|
implicit | public static Destination operator implicit(Source value) | Implicit conversion contexts and cast<Destination>(value). |
explicit | public static Destination operator explicit(Source value) | Only an explicit cast<Destination>(value). |
The destination type precedes operator. For example, use StringLike operator implicit(readonly byte* value); implicit operator StringLike(...) is not Xenon declaration syntax.
namespace ConversionExamples;
sealed struct StringLike
{
public readonly byte* Raw;
public static StringLike operator implicit(readonly byte* value)
{
return StringLike { value };
}
}
void Demo()
{
StringLike text = "Hello";
text = "World";
StringLike casted = cast<StringLike>("Again");
readonly byte* raw = "Hello";
}
The literal's type remains readonly byte*. No compiler knowledge of StringLike is involved: the conversion is selected because the surrounding context expects that destination. This example stores a pointer; it does not implement an owning string or allocate/copy text. A library requiring ownership must implement that behavior in its conversion body.
A constructor taking one argument does not automatically become an implicit or explicit conversion. Call the constructor directly, or declare a conversion operator whose body calls it.
Conversion contexts and ranking
An applicable implicit conversion can adapt a value for local initialization, simple assignment, function/method arguments, a return value, a field initializer, or a property/indexer setter value. It can also adapt operands to the parameters of a discovered user-defined operator. All the surrounding accessibility, mutability and lifetime checks still apply.
A conversion does not make its declaring type globally discoverable from unrelated operands. For example, converting pointers to StringLike can help match "Hello" + name once name is a StringLike with an applicable +. It does not turn "Hello" + "World" into a search through every string-like type.
// With StringLike declared above:
void Write(readonly byte* value) {}
void Write(StringLike value) {}
void DemoOverloads()
{
Write("Hello"); // Selects Write(readonly byte*): exact match.
}
If only Write(StringLike) exists, the implicit conversion may supply its argument. The ranking remains exact match → standard conversion → user-defined implicit conversion. Inaccessible conversion operators cannot create ambiguity or defeat an accessible candidate.
At most one user-defined conversion is allowed in a single conversion sequence. Standard conversions may match the source to the conversion's parameter and its return value to the destination. Those steps cannot themselves insert another user conversion. Equally good accessible conversion candidates are ambiguous.
// If the only conversions are A -> B and B -> C:
C value = a; // Invalid: requires two user-defined conversions.
Use separate, explicitly typed steps when two conversions are intentional. A cast<C>(a) also cannot silently chain two user conversions. Built-in casts take precedence; when no built-in cast applies, an explicit cast considers accessible implicit and explicit conversion operators.
Conversion identity and conflicting declarations
The destination matters for conversion identity. A struct can provide distinct conversions such as Source → A, Source → B and Source → C, even when they share a source parameter type.
Do not declare both implicit and explicit for the same effective source/destination value-type pair. This is a declaration error even when one operator is in the source struct and the other is in the destination struct, or when one side comes from XELIB. An implicit conversion already participates in an explicit cast. Passing the source by reference does not avoid this conflict.
// Invalid conflicting declarations:
struct A
{
public static B operator implicit(A value) { return B(); }
}
struct B
{
public static B operator explicit(A value) { return B(); }
}
Duplicate same-kind signatures inside one struct follow normal duplicate-declaration rules. If two different owners provide equally good accessible conversions of the same kind, use-site resolution can be ambiguous. Distinct source/destination pairs are not merged.
An identity conversion such as S operator implicit(S value) is rejected. Adding a reference to the source parameter does not change its underlying source value type for this rule.
Pointer and reference restrictions
| Destination category | implicit | explicit |
|---|---|---|
| Ordinary value type | Allowed when all declaration and conversion rules are satisfied. | Allowed when all declaration and conversion rules are satisfied. |
Raw pointer, such as void* or readonly byte* | Rejected. | Allowed under ordinary pointer/lifetime rules. |
| Function pointer | Rejected. | Uses the ordinary function-pointer and callable return rules. |
T& or readonly T& | Rejected. | Rejected. |
These are destination restrictions. A raw-pointer source is allowed, as in the string-like example, and a source may be passed by reference. Creating a converted value and binding a readonly reference to that value follows normal reference and temporary-lifetime rules; it does not permit a conversion operator to return a reference.
namespace HandleExamples;
struct NativeHandle
{
public void* Raw;
public static void* operator explicit(readonly NativeHandle& value)
{
return value.Raw;
}
}
void* Unwrap(NativeHandle value)
{
return cast<void*>(value);
}
void* raw = handle; is invalid with only that explicit conversion. The cast runs the conversion body; it does not extend the pointee's lifetime or transfer ownership automatically. A conversion body must satisfy the same rules as an ordinary function returning that pointer.
Copy, move, borrow and exceptions
Operators use ordinary callable argument passing. A by-value parameter may require a permitted copy or an explicit move. A readonly S& parameter borrows for reading; an S& parameter requires a mutable reference-compatible operand. An operator call never grants extra mutability or silently moves a named value.
For a move-only type, left + right cannot hide ownership transfer to by-value parameters. Supply move where an ordinary call requires it, or design a borrowing operator signature. The same rule applies to conversion operators.
Converted argument values and getter results have the usual temporary lifetimes. Passing a temporary by value transfers it according to ordinary call rules; borrowing it keeps its cleanup at the appropriate full-expression boundary. Exceptions use the same argument-transfer and cleanup machinery as normal functions.
Ordinary operators may return references when their reference-return provenance is valid, but conversion operators cannot. A returned reference cannot make a local variable or expired temporary outlive its storage. See references, move semantics and destructors.
Generic structs and source-less libraries
Operators can use the generic parameters of their containing struct. They specialize together with that struct. The required operand is the corresponding open self type, such as Box<T> inside Box<T>; declaring an operator only on Box<int> inside the general Box<T> declaration does not meet the self-operand rule.
namespace GenericOperatorExamples;
struct Box<T>
{
public T Value;
public Box(T value) { Value = move value; }
public static Box<T> operator implicit(T value)
{
return Box<T>(move value);
}
}
void Demo()
{
Box<int> value = 42;
}
This uses the struct's T; it does not declare independent generic parameters on the operator. Generic operators/conversions must still satisfy the normal constraints, copy/move rules and conflict checks after specialization. Operators are not additional structural-template constraint declarations.
XELIB libraries preserve operator kinds, signatures and implementations for consumers without the original source files, including generic conversions. Properties, indexers and other methods do not change which conversion declaration a specialization represents.
Editor hover, definition and reference navigation use the selected source-level operator/conversion symbol where supported. Native implementation identifiers do not replace the operator syntax in user-facing signatures.
Common errors and how to correct them
| Attempt | Why it fails / correction |
|---|---|
S operator +(S a, S b) without static | Operators must be static. |
static S operator +(int a, int b) inside S | Neither operand is the declaring struct. Returning S is insufficient. |
Unary operator * or unary operator & | Only their binary forms are supported. |
operator += | Declare binary operator + and make its result assignable to the target. |
operator == with three parameters | Equality has exactly two operands. |
static S* operator implicit(S& value) | An implicit conversion cannot return a raw pointer. Use an explicit conversion if ordinary pointer rules allow it. |
static S& operator explicit(Other value) | Conversion to a reference is unsupported, including in an explicit cast. |
| Both conversion kinds for the same value-type pair | Keep one declaration; implicit already works in casts. |
| An explicit conversion used by plain assignment | Write cast<Destination>(value). |
| A chain of two user conversions | Introduce a separately typed intermediate step. |
1 + s after declaring only S + int | Operand order matters. Declare an applicable reversed overload if that operation belongs in the API. |
Consult Operators for built-in expression behavior and function overloads for the shared callable-resolution rules.
Await and completion operators
The await, resolve and reject operators define suspension and terminal completion using ordinary library types. Await takes an operand, optional writable storage<T>& and function void() continuation and returns bool. Completion operators return void and take a writable target reference. See Await and resumable functions for the protocol, lifetime rules and diagnostics.