C++20 concept for complex floating point types

1.7k Views Asked by At

I am trying to learn concepts in C++20 and I have a class that represents a data sample. I want to restrict this class to accept only floating point types, or complex floating point types but I can't seem to figure out how to handle complex values with concepts.

Without concepts this is simple, but it allows way too many other data types that I don't want to allow.

Example without concepts:

template <typename T>
class Sample
{
    // ...
};

int main()
{
    // This compiles
    Sample<double> s1;
    Sample<complex<double>> s2;

    // This also compiles (but I don't want it to!)
    Sample<int> s3;
    // This compiles as well (again, I don't want it to!)
    Sample<complex<int>> s4;
}

With concepts I can easily restrict it to just take floating point values but then it doesn't work with complex values.

template<floating_point T>
class Sample
{
    // ...
};

int main()
{
    // This compiles
    Sample<double> s1;
    Sample<float> s2;

    // This does NOT compile (but I do want it to!)
    Sample<complex<double>> s3;
}

How can I create a concept to restrict the template to work with both real and complex floating point values?

4

There are 4 best solutions below

3
cigien On BEST ANSWER

Here's one solution that uses a partial specialization to check if T is a specialization of std::complex for floating point types:

template <typename T>
struct is_complex : std::false_type {};

template <std::floating_point T>
struct is_complex<std::complex<T>> : std::true_type {};

With this, you can write the concept:

template <typename T>
concept complex = std::floating_point<T> || is_complex<T>::value;

Here's a demo.

2
Paul Sanders On

A little experimentation shows that you can do this:

template <class T>
concept is_floating_point_or_complex = std::is_floating_point_v<T> || std::is_same_v <T, std::complex <double>>;

template<is_floating_point_or_complex T>
class Sample
{
    // ...
};

But it's not obvious how to avoid specialising std::complex in is_floating_point_or_complex (if indeed you want to).

Live demo

0
aschepler On

Here's code using a helper type trait class with partial specialization, to determine if a type is complex with floating point coordinates.

#include <type_traits>
#include <concepts>
#include <complex>

template <typename T>
struct is_complex_floating_point : public std::false_type {};

template <typename T>
struct is_complex_floating_point<std::complex<T>>
    : public std::bool_constant<std::is_floating_point_v<T>>
{};

template <typename T>
concept real_or_complex_floating_point =
    std::floating_point<T> || 
    is_complex_floating_point<std::remove_const_t<T>>::value;

template<real_or_complex_floating_point T>
class Sample
{
    // ...
};

I used the remove_const_t because std::floating_point is satisfied by const float, etc., meaning your existing Sample (with constrained parameter) would allow Sample<const double>, etc. So the concept is defined to accept const std::complex<T>, making Sample<const std::complex<double>> etc. work. If that shouldn't be considered valid, you can remove the remove_const_t part and possibly consider also restricting your template to forbid cv-qualified types.

[As @cigien noticed in their solution, the partial specialization of is_complex_floating_point is simpler to write using the std::floating_point concept. An exercise for the reader. ;) ]

0
kkm -still wary of SE promises On

The solution presented here is not essentially different from, or in any sense better than the one posted by cigien. The goal is to demonstrate an alternative technique using a SFINAE-like property of the concept_id.

A concept variable template is syntactically a regular implicitly inline constexpr bool template variable with a constraint-expression as its value. The semantic deviation from the regular SFINAE rule is that a substitution failure results in the concept variable evaluated to false (N4868 §7.5.7.1(6)), as opposed to being considered non-viable in the SFINAE case:

The substitution of template arguments into a requires-expression may result in the formation of invalid types or expressions in its requirements or the violation of the semantic constraints of those requirements. [This is how SFINAE works. -kkm] In such cases, the requires-expression evaluates to false; it does not cause the program to be ill-formed.

With this in mind, a complex float concept may be defined as

template <typename T, typename F = T::value_type>
concept Complex = (std::is_floating_point_v<F> &&
                   std::is_same_v<std::remove_cv_t<T>, std::complex<F>>);

The template argument F is substituted with the base type of std::complex<F>, exposed via that template's member type alias value_type. The concept expression asserts that F is indeed a floating point type, and that the T itself is a specialization of std::complex<F> up to CV-qualification.

Another useful restriction is defined in (§13.7.9(5) Note 1):

... A concept cannot be explicitly instantiated, explicitly specialized, or partially specialized.

This means that in practice the template argument F is a constant, preventing any "creative use" in case such a concept is defined in a general-purpose library. This is in contrast to a regular inline template variable, where the parameter F might be explicitly specified by the user of the template.

The RealOrComplex concept is defined trivially as

template <typename T>
concept RealOrComplex = std::is_floating_point_v<T> || Complex<T>;

Below is a full working example, compiled (or failing to, if you wish, depending on the definition of TRY_COMPILING_THIS) with GCC, Clang and MSVC.

#define _SILENCE_NONFLOATING_COMPLEX_DEPRECATION_WARNING 1  // Otherwise clang complains a deluge.

#include <complex>
#include <iosfwd>
#include <iostream>
#include <type_traits>

template <typename T, typename F = T::value_type>
concept Complex = (std::is_floating_point_v<F> &&
                   std::is_same_v<std::remove_cv_t<T>, std::complex<F>>);
template <typename T>
concept RealOrComplex = std::is_floating_point_v<T> || Complex<T>;

template <RealOrComplex T>
class Sample {
 public:
  constexpr explicit Sample(T value) noexcept : _value(value) {}

  friend std::ostream& operator<<(std::ostream& os, const Sample& sample) {
    return os << sample._value, os;
  }

 private:
  T _value;
};

int main() {
  using std::complex;
  using namespace std::complex_literals;

  std::cout
    << "CTAD:\n"
    << "  R(32)    : " << Sample{3.14f} << "\n"
    << "  R(64)    : " << Sample{3.14} << "\n"
    << "  C(32,32) : " << Sample{3.0f + 0.14if} << "\n"
    << "  C(64,64) : " << Sample{0.14 + 3.0i} << "\n\n";

  std::cout
    << "Explicit:\n"
    << "  R(32)    : " << Sample<float>{3.14f} << "\n"
    << "  R(64)    : " << Sample<double>{3.14} << "\n"
    << "  C(32,32) : " << Sample<complex<float>>{3.0f + 0.14if} << "\n"
    << "  C(64,64) : " << Sample<complex<double>>{0.14 + 3.0i} << "\n";

#define TRY_COMPILING_THIS 0
#if TRY_COMPILING_THIS
  // MSVC: no matching function for call to 'Sample(int)'
  // clang: constraints not satisfied for class template 'Sample' [with T = int]
  Sample{1};
  Sample<int>{1};

  //  There is no suffix for an `int` in `complex_literals`, so that braces must
  //  be doubled: the inner pair CTAD-constructs a `complex<int>`, the outer is
  //  the usual uniform initialization syntax. See the last line of the three
  //  where the `complex<int>` argument to `Sample.ctor` is constructed explicitly.
  //  All three definitions are equivalent up to CTAD vs. explicit specialization.
  Sample{{1, 1}};
  Sample<complex<int>>{{1, 1}};
  Sample<complex<int>>{complex<int>{1, 1}};
#endif
}