Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Directly Embed Type Constraints in Cpp2 Type Definitions #1353

Open
Mr-enthalpy opened this issue Jan 2, 2025 · 5 comments
Open

Directly Embed Type Constraints in Cpp2 Type Definitions #1353

Mr-enthalpy opened this issue Jan 2, 2025 · 5 comments

Comments

@Mr-enthalpy
Copy link

Summary:
I propose introducing a feature in Cpp2 that allows users to express constraints on types themselves directly in the type definition. This could be done using the syntax name:type:concept={...} or name:type is concept = {...}, allowing the defined type to automatically satisfy specific constraints (concepts).

Problem:
Currently in Cpp2, we can constrain deduced types (e.g., number: _ is std::regular = ...), but there is no straightforward way to enforce constraints on the type itself. This feature would close that gap and enable a more declarative way to ensure types meet certain requirements.

Proposed Syntax:

  1. Current Cpp2 Syntax:

    Point :type =
    {
        x: i32;
        y: i32;
        draw: (this) = ....
    }

    This generates the following Cpp1 code:

    struct Point;
    
    struct Point {
        int x, y;
        void draw();
    };
    
    void draw() {
        // implementation
    }
  2. New Feature:
    The proposed feature would allow adding constraints directly to the type definition:

    Point :type :drawable =
    {
        x: i32;
        y: i32;
        draw: (this) = ....
    }

    This would generate the following Cpp1 code with a static_assert to ensure that Point satisfies the drawable concept:

    struct Point;
    
    struct Point {
        int x, y;
        void draw();
    };
    
    void draw() {
        static_assert(drawable<Point>);
        // implementation
    }

Benefits:

  1. Self-Documenting:
    The constraint directly on the type definition acts as documentation. It makes the type's requirements clear and self-contained, so readers immediately understand what the type should satisfy without needing to refer to separate documentation.

  2. Declarative Constraints:
    This feature allows constraints to be declared directly within the type definition, improving code readability. Instead of adding static_assert or other checks manually in the implementation, constraints are declared in one place, making the code more concise and less error-prone.

  3. Aligns with Existing Features:
    Cpp2 already supports type constraints on deduced types using the syntax name: _ is concept = .... Extending this syntax to constrain the type itself ensures consistency within the language and provides a more flexible and expressive way to enforce type contracts.

@gregmarr
Copy link
Contributor

gregmarr commented Jan 3, 2025

I assume for void draw() { you mean void Point::draw() {.

@gregmarr
Copy link
Contributor

gregmarr commented Jan 3, 2025

So this would essentially add a static_assert to every member function definition? What if the type doesn't have any member functions, not even a constructor, only public data members?

@Mr-enthalpy
Copy link
Author

Mr-enthalpy commented Jan 3, 2025

So this would essentially add a static_assert to every member function definition? What if the type doesn't have any member functions, not even a constructor, only public data members?

Yes, you are right. void draw() should indeed be written as void Point::draw(), I made a mistake there.

The idea behind this proposal is to insert a static_assert into every member function definition of the type. For types without any member functions—only public data members, for instance—this becomes tricky. You're correct that there isn't a perfect solution for this case. One possible approach could be to restrict the use of self-imposed constraints for such types. While this might seem inconsistent, it's worth noting that these types are quite rare. Types with only public data members, without any complex behavior or invariants to enforce, don't usually require constraints.

An example of this would be something like:

point_3d :type = { x: ui64; y: ui64; z: ui64; };

However, in most cases, the types we define are more complex, with member functions and invariants that benefit from such constraints. For those cases, I believe handling this situation would be straightforward.

@Mr-enthalpy
Copy link
Author

So this would essentially add a static_assert to every member function definition? What if the type doesn't have any member functions, not even a constructor, only public data members?

The reason for inserting static_assert within the member function definitions rather than externally is that, in the case of template classes with specializations (both full and partial), the self-imposed constraints might differ. Inserting the static_assert directly within the member function definitions naturally corresponds to different specialized versions:

I'm not sure whether template specialization and partial specialization are already introduced in Cpp2, so the following code will be written in Cpp1 syntax:

template <typename T, typename U>
struct Printer;

template <typename T, typename U>
struct Printer 
{
    void print();
};

template <typename T>
struct Printer<T, int> 
{
    void print();
};

template <>
struct Printer<int, int>  
{
    void print()
    {
        static_assert(printable_0<Printer<T, U>>); 
    }
}; 

template <typename T>
struct Printer<T, T> 
{
    void print();
};

template <typename T, typename U>
void Printer<T, U>::print() 
{
    static_assert(printable_1<Printer<T, U>>); 
}

template <typename T>
void Printer<T, int>::print() 
{
    static_assert(printable_2<Printer<T, int>>); 
}

template <typename T>
void Printer<T, T>::print() 
{
    static_assert(printable_3<Printer<T, T>>); 
}

As shown above, we can apply different constraints for partial and full specializations. However, I'm not sure whether this level of granularity is necessary—perhaps we don't need to apply such fine-grained self-imposed constraints. It might be sufficient for all specializations to share the same concept.

@DyXel
Copy link
Contributor

DyXel commented Jan 4, 2025

You could probably reuse the recently introduced syntax of is for this. e.g.: Point: type is drawable = rather than Point :type :drawable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants