-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Summary of issue:
This question seeks to define the model for integrating C++ operator overloads into Carbon. The central challenge is to create a predictable system that reconciles two different paradigms: C++'s complex, context-sensitive overload resolution (including ADL and implicit conversions) and Carbon's interface-based impl
lookup. The decision required is to specify how these two mechanisms should interact. Should Carbon's impl
lookup delegate to C++'s lookup, should impl
s be generated statically when a type is imported, or should they be synthesized on-demand? Furthermore, how should this system prioritize between an imported C++ operator and a user-defined Carbon impl
for the same C++ type? The goal is a design that makes using C++ operators in Carbon feel seamless and intuitive.
Details:
Problem Statement
When a Carbon user imports a C++ type, they should be able to use its overloaded operators naturally. For example, if a user-defined C++ class MyProject::Widget
with an overloaded operator==
is imported, a Carbon developer should be able to write widget1 == widget2
and have it work as expected.
This requires bridging two different systems:
- C++: Uses
operator
overloading with a complex lookup mechanism that includes Argument-Dependent Lookup (ADL). - Carbon: Uses dedicated operator interfaces (e.g.,
EqWith
) and selects an implementation viaimpl
lookup.
The trigger for this special C++ interop logic is any operation involving a Cpp
associated type. This is any type whose definition involves an imported C++ type, such as:
- The C++ type itself:
Cpp.Widget
- A generic Carbon type with a C++ type parameter:
MyVector(Cpp.Widget)
- A Carbon struct with a C++ member:
MyStruct { x: Cpp.Widget }
- A pointer to a C++ type:
Cpp.Widget*
We need to define a clear and predictable model for how a C++ operator
overload is selected and used when a Carbon operator is invoked on C++ types.
Goals and Key Considerations
Any proposed solution must account for the following:
- Triggering: When should we perform C++ operator lookup versus Carbon
impl
lookup? - Type Handling: How should the system handle a mix of C++ types, Carbon types, and primitive types as operands or return values?
- User Extensibility: Users should be able to write their own explicit
impl
for a Carbon operator interface involving C++ types, and the selection between the user'simpl
and an imported C++ operator should be predictable. - Ownership: The solution must respect both Carbon's orphan rule (
impl
must be in the same library as the interface or the type) and C++'s module ownership rules. - Consistency: The proposed model should be consistent with how other C++ features are handled, such as destruction (
Destroy
), conversions (As
), and copy/move operations.
Proposed Alternatives
1. Blanket impl
A single, generic impl
would be defined next to each operator interface (e.g., impl Cpp.T as AddWith(...)
). This impl
would be responsible for triggering C++ lookup and overload resolution at compile time.
- Pros: Conceptually simple; keeps the logic centralized with the Carbon interface.
Destroy
is planned to use this approach. - Cons: May be difficult to reconcile with C++'s ADL rules. It creates a potential conflict if a user wants to provide a more specific
impl
for a C++ type.
2. Generate impl
on Type Import
When a C++ type is imported, we would automatically generate a corresponding Carbon impl
for each C++ operator it supports within the Cpp
namespace.
- Pros: The
impl
is tightly coupled to the imported C++ type. - Cons:
- Impractical to discover all operators: C++ operator lookup is context-dependent (relying on ADL), so it's often impossible to find all relevant operators when a type is first imported without knowing how it will be used.
- Inefficient eager generation: This approach generates code for all of a type's operators upfront, even those that are never used, leading to slower compilation and code bloat.
- Ownership challenges: It's unclear which library "owns" the generated
impl
, potentially leading to duplicate definitions.
3. Hook impl
Lookup and Synthesize a Witness
Instead of generating a static impl
, we would hook into Carbon's impl
lookup process. If the types involve Cpp
, we would perform C++ operator lookup and, if successful, dynamically synthesize a "witness" (a proof of the impl
) without creating an actual impl
declaration.
- Pros: Highly dynamic and avoids static
impl
generation. It's as if a non-genericimpl
exists for every valid C++ operator call. - Cons: The behavior could feel "magic" and might be difficult to debug. It's unclear how this would interact with explicit user-defined
impl
s, which might be preferred over the C++ operator.
4. Hook impl
Lookup, Synthesize impl
, and use Carbon Selection
This is a hybrid "just-in-time" approach that leverages the strengths of both systems.
- When an operator is used on a
Cpp
associated type, trigger C++ operator lookup and overload resolution. - If the result is ambiguous, raise a compile-time error.
- If a single best function is found, convert its C++ signature into a temporary, specific Carbon
impl
. For example, C++'sauto operator+(A<int>, B<int>) -> C<int>;
becomes animpl
forCpp.A(i32) as AddWith(Cpp.B(i32)) -> Cpp.C(i32)
. - This synthesized
impl
is then added to the set of candidates for regular Carbonimpl
selection.
-
Pros:
-
Robust and Predictable: It uses C++'s powerful overload resolution to find the right candidate but integrates it into Carbon's
impl
selection process. This allows user-definedimpl
s to compete with (and potentially override) C++ operators in a predictable, efficient way. -
Handles Complex C++ Rules (like Implicit Conversions): This model correctly leverages C++'s own rules without re-implementing them. For example, consider a C++
operator+(A, B)
and an implicit conversion fromA
toB
. When a Carbon user writesa + a
(wherea
isCpp.A
), C++ overload resolution will findoperator+(A, B)
as a valid candidate by applying the conversion. This approach then synthesizes a concreteimpl
forCpp.A as AddWith(Cpp.A)
for Carbon'simpl
selection.This reflects a key insight from the design discussion: while the synthesized
impl
is specific to the call, the underlying C++ lookup is powerful enough to behave as if a more generic rule existed. In more formal terms, it provides the power of animpl forall [T:! type] Cpp.A as AddWith(T)
, where C++'s own conversion rules determine the valid types forT
.
-
-
Cons: This is the most complex alternative to implement.
Recommended Approach
The discussion so far favors Alternative 4. It correctly delegates the initial lookup to C++'s established rules while allowing the final decision to be made by Carbon's impl
selection mechanism. This provides a predictable, "pay-for-what-you-use" model that correctly handles complex C++ features like implicit conversions and allows users to supplement C++'s behavior with their own Carbon impl
s.
This issue seeks a decision on whether to proceed with this design.