tjwrona
tjwrona

Reputation: 9035

C++20 concept for complex floating point types

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?

Upvotes: 5

Views: 2118

Answers (4)

kkm mistrusts SE
kkm mistrusts SE

Reputation: 5510

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
}

Upvotes: 1

aschepler
aschepler

Reputation: 72311

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. ;) ]

Upvotes: 2

cigien
cigien

Reputation: 60228

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.

Upvotes: 8

catnip
catnip

Reputation: 25388

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

Upvotes: 0

Related Questions