10 SWIG and C++20

10.1 Introduction

This chapter gives you a brief overview about the SWIG implementation of the C++20 standard. Work has only just begun on adding C++20 support.

Compatibility note: SWIG-4.1.0 is the first version to support any C++20 features.

10.2 Core language changes

10.2.1 Spaceship operator

SWIG supports the spaceship operator <=> in constant expressions. To simplify handling of the return value type, it is currently treated as an integer rather than std::strong_ordering, etc. In practice we think that should do the right thing in most cases.

SWIG also recognises operator<=> which can be wrapped if renamed. There is not currently any default renaming for the operator or any attempt to automatically map it to a three-way comparison operator in any of the target languages.

10.2.2 Lambda templates

SWIG parses lambda templates, but like non-templated lambdas, they aren't currently wrapped. For example:

auto templated_lambda_sum = []<typename T>(std::vector<T> v) {
  return std::reduce(v.begin(), v.end());
};

10.2.3 Constexpr destructors

Destructors that are declared constexpr are parsed and handled like any other constructor. For example:

class DtorA {
public:
  constexpr ~DtorA() {}
};

10.2.4 Abbreviated function templates

C++20 generalised the C++14 Generic lambdas idea to ordinary functions: an auto parameter type in a function declaration introduces an invented type template parameter, so the function becomes a function template. SWIG parses this form and the function is wrapped by instantiating it with %template, just like an explicit template. The return type must be explicit; SWIG cannot deduce auto return types without a trailing return type (the same restriction documented in Return type deduction).

%inline %{
int add1(auto a) { return a + 1; }
double scale(auto x, auto factor) { return x * factor; }
%}

%template(add1_int) add1<int>;
%template(scale_id) scale<int, double>;

In C++20 a concept type-constraint can qualify the auto, equivalent to declaring the invented type template parameter with a requires-clause. Each constrained auto parameter carries its own type-constraint:

%inline %{
#include <concepts>

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

int twice_numeric(Numeric auto x) { return x + x; }
double scale_mixed(Numeric auto x, Numeric auto factor) { return x * factor; }
%}

%template(twice_numeric_int) twice_numeric<int>;
%template(scale_mixed_id)    scale_mixed<int, double>;

A constrained auto return type is parsed using the same syntax. As with plain auto returns, SWIG can only wrap the function when an explicit trailing return type is provided; without one the function is ignored with a warning since SWIG cannot deduce the return type:

Numeric auto half(int x) -> int { return x / 2; }                    // wrappable
Numeric auto cube_constrained(Sized auto x) -> int { return x*x*x; } // wrappable
Numeric auto times2(int x) { return x * 2; }                         // ignored with warning
Numeric auto times3(int x);                                          // ignored with warning

The auto parameter form combines freely with an explicit template<...> template parameter list. Per the C++20 standard each auto parameter introduces a distinct invented type template parameter that is appended to the explicit template parameter list. When instantiating with %template the user supplies one argument for each parameter in order - first the explicit parameters, then one for each auto parameter in declaration order:

%include <std_string.i>

%inline %{
#include <string>

template<typename T>
T tag(T prefix, auto count) { return prefix + std::to_string(count); }
%}

%template(tag_si) tag<std::string, int>;

Above, tag has two template parameters: the explicit T and an invented type template parameter for the auto parameter. The %template arguments <std::string, int> bind them in that order, so T becomes std::string (the type of prefix) and the invented parameter becomes int (the type of count). Calling tag_si("v", 3) returns "v3". Type-constraints and trailing return types mix the same way.

A variadic explicit template pack can also be combined with one or more auto parameters. Per [dcl.fct]/19 the invented type template parameter for each auto is appended after the explicit list, so when the explicit list ends in a pack the invented sits past it. The %template argument list is bound positionally: leading non-variadic parameters first, the variadic pack absorbs the middle args, and trailing args bind to the invented parms in declaration order.

%include <std_string.i>

%inline %{
#include <string>

template<typename... Ts>
std::string f_mix(auto x, Ts... ys);
%}

%template(f_mix_isd) f_mix<int, std::string, double>;

Here f_mix has the invented type template parameter for x appended after Ts.... The three %template arguments bind as Ts = {int, std::string} (absorbed by the pack) and the trailing double binds to the invented parm for x, giving the wrapper signature f_mix(double x, int y1, std::string y2).

The auto placeholder may carry the usual parameter decorations - reference, pointer, forwarding reference, and CV-qualifiers.

int h(auto& x);                   // reference to invented type
int i(auto* x);                   // pointer to invented type
int j(auto&& x);                  // forwarding reference
int k(const auto x);              // const-qualified by value
int l(const auto& x);             // const reference
int m(Numeric auto& x);           // constrained reference
int n(const Numeric auto& x);     // const-qualified constrained reference

template<typename T>
T o(T x, const auto& y);          // decorated auto mixed with explicit head

Compatibility note: SWIG-4.5.0 is the first version to parse and support abbreviated function templates with auto parameters, including the constrained auto form in both parameter and return type positions, mixing with an explicit template parameter list, and CV-qualifier / reference / pointer decorations on the auto placeholder.

10.2.5 Concepts and requires-clauses

SWIG provides support for parsing C++20 concept declarations and requires-clauses, in both the trailing position (after the parameter list) and the prefix position (between the template parameter list and the declarator). Constraints are accepted on function templates, class templates and member function templates; both constructs are consumed by the parser and a constrained template wraps as if it were unconstrained. This is sufficient for SWIG to accept headers that use concepts to express template constraints, without the user having to strip them by hand. Applying %template to a concept is rejected with an error, since concepts are not types and cannot be instantiated.

The following interface parses cleanly and wraps each instantiation as an ordinary function template instantiation:

%inline %{
#include <concepts>

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Trailing requires-clause.
template<typename T>
T cube(T x) requires Numeric<T> {
  return x * x * x;
}

// Prefix requires-clause.
template<typename T>
requires Numeric<T>
T quad(T x) {
  return x * x * x * x;
}
%}

%template(cube_int) cube<int>;
%template(quad_int) quad<int>;

Compound prefix constraints joined by && or || and parenthesised constraint subexpressions are also accepted - for example requires Numeric<T> && SmallNumeric<T> or requires (Numeric<T> || std::same_as<T, bool>).

A requires-expression itself - the new C++20 primary that lists a sequence of requirements in braces - can serve as the primary of a constraint, giving the "double requires" form requires requires (...) { ... }:

template<typename T>
T add(T a, T b) requires requires (T t) { t + t; } {
  return a + b;
}

%template(add_int) add<int>;

Equivalently, the requires-expression can be lifted into a named concept and reused as a constraint:

template<typename T>
concept Summable = requires (T t) { t + t; };

template<typename T>
T sum_pair(T a, T b) requires Summable<T> {
  return a + b;
}

%template(sum_pair_int) sum_pair<int>;

The body of a requires-expression can mix simple-requirements (expr;) with compound-requirements that carry a trailing return-type-requirement ({ expr } -> type-constraint;), for example:

template<typename T>
concept AddableSame = requires(T a, T b) {
  { a + b } -> std::same_as<T>;
};

template<typename T>
T add_same(T a, T b) requires AddableSame<T> {
  return a + b;
}

%template(add_same_int) add_same<int>;

A class template can carry a prefix requires-clause on its template parameter list; the constraint propagates to every instantiation:

template<typename T>
requires Numeric<T>
class NumericBox {
  T value;
public:
  NumericBox(T v) : value(v) {}
  T cube() const { return value * value * value; }
};

%template(NumericBoxInt) NumericBox<int>;

Ordinary methods and constructors of an unconstrained class template can carry their own trailing requires-clause. Both int and double satisfy Numeric<T>, so both instantiations of CheckedBox below succeed:

template<typename T>
class CheckedBox {
  T value;
public:
  CheckedBox() : value(T()) {}
  CheckedBox(T v) requires Numeric<T> : value(v) {}
  T get() const { return value; }
};

%template(CheckedBoxInt)    CheckedBox<int>;
%template(CheckedBoxDouble) CheckedBox<double>;

Member function templates of an ordinary (non-templated) class can carry constraints in either the trailing or prefix position, including on static member function templates:

class Calculator {
public:
  template<typename T>
  T cube(T x) const requires Numeric<T> { return x * x * x; }

  template<typename T> requires Numeric<T>
  T quad(T x) const { return x * x * x * x; }

  template<typename T>
  static T sum(T a, T b) requires requires(T t) { t + t; } { return a + b; }
};

Function templates that share a name and differ in arity may each carry their own requires-clause; SWIG dispatches by argument count at runtime and the constraints do not affect that path:

template<typename T> requires Numeric<T>
T accumulate(T a) { return a + 1; }

template<typename T> requires Numeric<T>
T accumulate(T a, T b) { return a + b; }

A class template can have a structural partial specialization (such as T against T*) whose template parameter list additionally carries a requires-clause. SWIG selects between primary and partial spec on the structural pattern alone; the requires-clause is captured but does not participate in selection. The two specs wrap as distinct types with their own method sets:

template<typename T>
struct Storage {
  int primary_method() const { return 100; }
};

template<typename T> requires Numeric<T>
struct Storage<T*> {
  int partial_method() const { return 200; }
};

%template(StorageInt)    Storage<int>;
%template(StorageIntPtr) Storage<int*>;

Templated lambdas accept requires-clause in all positions, such as:

auto prefix = []<typename T> requires Numeric<T> (T x) { return x + x; };
auto with_ret = []<typename T>(T x) -> T requires Numeric<T> { return x + x; };

Limitations:

The captured constraint does not drive wrapper code generation: overload resolution still happens at C++ compile time, by which point SWIG has already produced the wrapper. The generated bindings are therefore the same as for an unconstrained template, for any constraint that the underlying C++ compiler accepts.

Compatibility note: SWIG-4.5.0 is the first version to support C++20 concepts.

10.2.6 Type constrained template parameters

A type-constraint may also stand in place of typename / class in a template parameter list. For example, given the following concept:

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

the two function declarations below are equivalent and both a C++ compiler and SWIG treats them interchangeably:

// shorthand
template<Numeric T>
T cube(T x);

// fully spelt out
template<typename T> requires Numeric<T>
T cube(T x);

The shorthand fits anywhere typename / class would appear: ::-qualified concept-ids, variadic packs, mixed parameter lists, default arguments and class templates are all accepted, such as:

template<Numeric T>                     T cube(T x);                  // single
template<nest::Integral T>              T half(T x);                  // ::-qualified
template<Numeric T, typename U>         T scale(T x, U factor);       // mixed
template<Numeric T = int>               T identity(T x);              // default arg
template<Numeric... Ts>                 int count_numeric(Ts...);     // variadic pack
template<Numeric T> class Box { T v; };                               // class template

Wrapping procedes by remapping the template parameter from Numeric T to typename T. If the concept definition has not been parsed by SWIG the remap is still applied silently, since SWIG always tries a best effort approach to wrapping partial type information. The generated wrapper emits the templated call literally and the C++ compiler resolves the constraint when compiling the generated wrapper code. For example, the following wraps cleanly even though SWIG has not seen the declaration of Numeric:

// SWIG has not parsed 'Numeric' anywhere
template<Numeric T>
T cube(T x) { return x * x * x; }
%template(cube_int) cube<int>;

The examples above all use single parameter concepts, but a type-constraint may itself be a concept-id with explicit template arguments. This is the form most often seen with the STL and SWIG also supports these two parameter concepts such as std::convertible_to<From, To>, std::same_as<T, U>, std::derived_from<Derived, Base> and std::predicate<F, Args...>.

The rule the C++ standard gives is that the constrained template parameter is prepended to the list in angle brackets to form the full concept-id, becoming the first argument; the arguments written inside the brackets supply the remaining arguments. So the shorthand:

template<std::convertible_to<int> T>
T to_int(T x);

is equivalent to the fully spelt out form:

template<typename T> requires std::convertible_to<T, int>
T to_int(T x);

that is, T is constrained to be convertible to int. A user defined two parameter concept follows the same rule:

%inline %{
template<typename T, typename U>
concept Pair = std::convertible_to<T, U>;

template<Pair<int> T>        // requires Pair<T, int>
int first_int(T x) { return (int)x; }
%}

%template(first_int_d) first_int<double>;

Compatibility note: SWIG-4.5.0 is the first version to parse template parameters carrying a type-constraint, including the template-id form above.

10.2.7 Constrained alias templates

A C++11 alias template (see Alias templates in the C++11 chapter) may carry a C++20 concept constraint on its template parameters, in either the type-constraint shorthand form or the requires-clause long form:

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<typename T>
class Box { T v; public: Box(T x):v(x){} T get() const { return v; } };

// shorthand
template<Numeric T>
using NumBox = Box<T>;

// fully spelt out (equivalent)
template<typename T> requires Numeric<T>
using ReqBox = Box<T>;

SWIG accepts both forms. Both aliases resolve to the same underlying Box<T>, and the generated wrappers are identical to those for an unconstrained alias. Wrapping uses the same two-step pattern as any other alias template as described in Alias templates - instantiate the underlying template once with %template(Name), then register each alias with an empty %template():

%template(BoxInt) Box<int>;
%template() NumBox<int>;
%template() ReqBox<int>;

SWIG does not enforce the constraint itself; the C++ compiler resolves it when compiling the generated wrapper. An unparsed concept name in the parameter list is silently remapped to typename, following SWIG's best effort policy for partial type information.

Compatibility note: SWIG-4.5.0 is the first version to parse the requires-clause form on alias templates.

10.2.8 Variable templates initialised from a requires-expression

C++14 already permitted a variable template (see Variable templates in the C++14 chapter). C++20 adds the requires-expression, a new primary that evaluates to bool and whose body lists requirements for a type, and these two pair naturally: a variable template can be declared constexpr bool and initialised from a requires-expression to give a compile time predicate over the template parameter. This is the variable template equivalent of the type trait _v pattern (such as std::is_integral_v) that <type_traits> uses to expose a class template trait as a bool variable, written without needing a separate concept declaration.

%inline %{
template<typename T>
constexpr bool Addable = requires (T t) { t + t; };

struct NotAddable {};   // empty struct, no operator+
%}

%template(addable_int)        Addable<int>;          // read only bool = true
%template(addable_notaddable) Addable<NotAddable>;   // read only bool = false

SWIG cannot evaluate the requirements at parse time; the C++ compiler decides the value at instantiation time and the wrapper exposes the resulting bool. A requires-expression is always typed bool by the standard, so this idiom is effectively limited to bool-typed variable templates - any other declared type would have to be implicitly constructible from bool to compile, and is not idiomatic.

Compatibility note: SWIG-4.5.0 is the first version to parse a requires-expression in expression position.

10.2.9 Class template argument deduction

C++20 (P1816) extended class template argument deduction (CTAD) to aggregates - which have no constructors to deduce from - so an aggregate variable such as the Overloaded helper can be declared with a bare class template name and no explicit deduction guide:

Overloaded ov{ IntCase{}, DoubleCase{} };   // C++20 aggregate CTAD

SWIG handles aggregate CTAD exactly as it handles any other CTAD variable: it performs no template argument deduction, so it issues Warning 347 and skips the variable. See Class template argument deduction in the C++17 chapter for details.

10.3 Preprocessor changes

10.3.1 __VA_OPT__()

Support for __VA_OPT__() was added in SWIG 4.3.0.

10.4 Standard library changes