This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

3933. P1467R9 accidentally changed the signatures of certain constructors of std::complex

Section: 28.4.3 [complex] Status: New Submitter: Jiang An Opened: 2023-05-16 Last modified: 2023-06-01

Priority: 4

View other active issues in [complex].

View all other issues in [complex].

View all issues with New status.

Discussion:

In C++20 and earlier revisions, there are constructors taking two floating-point numbers by value in explicit specializations of std::complex for standard floating-point types. Since P1467R9 has removed these explicit specializations, the corresponding constructor in the primary template that takes arguments by const T& are used instead. As a result, the following program becomes ill-formed after the changes.

#include <complex>

int main()
{
  volatile double x = 0.0;
  std::complex<double> z{x, x}; // ill-formed due to P1467R9 because const double& cannot be bound to a volatile double lvalue
}

Currently, libstdc++ has implemented complex specializations for extended floating-point types, but the corresponding constructors of these specializations takes two arguments by value, which is consistent with old specializations.

It seems that it's unintended to change the signatures of these constructors. Perhaps we should restore the signatures for required specializations.

Daniel:

Not only constructors are affected, but also all assignment operators taking the value_type as parameter and I suggest that LEWG should have a look at this issue.

[2023-05-20; Daniel comments and suggests wording]

The wording below attempts to restore the exact previous behaviour: For all floating-point types the function parameter types are "by value" and for other types are "by const reference". The wording adds for specification purposes a dependency to the concept std::floating_point, but that doesn't mean that an implementation couldn't realize the required effects without usage of concepts or the <type_traits> header.

Note that we have already precedence for similar cases where we later reintroduced concept requirements to library parts where no concept dependencies had exist before, such as the iterator_traits specialization for pointers (to object) or the additional constraints of set's member function iterator erase(const_iterator position) or the constraint for reverse_iterator::operator->(), just to name a few.

One alternative approach could be to switch to "by-value" signatures only for the affected signatures. This could affect user-defined floating-point-like types such as those with an arbitrary precision, therefore I started with the most conservative approach restoring the original effects that was present in the working draft N4910 and older ones. It might we worth pointing out that the existing "setter" functions imag and real have always been using "by-value" signatures for all specializations.

There exists also the possible argument to close this issue as NAD based on the argument that all existing non-member operators taking a value_type argument had always been defined to use const T& as parameter (such as the operator@(const T& lhs, const complex<T>& rhs) forms).

My main argument to solve this issue as shown below is based on the ground that the refactoring done by P1467R9 was mainly inspired to simplify the existing wording and to make it more easy to integrate the addition of the extended floating-point types here, as quoted from P1467R9 section 6.6. <complex>:

[…] The explicit specializations of std::complex<T> are removed. The only differences between the explicit specializations was the explicit-ness of the constructors that take a complex number of a different type.

This issue has some overlap with LWG 3934, which suggests a yet missing specification for the assignment operator taking the value_type as parameter.

[2023-06-01; Reflector poll]

Set priority to 4 after reflector poll.

Several votes for NAD, as this only affects volatile arguments, so this might even be an accidental improvement.

Proposed resolution:

This wording is relative to N4950.

  1. Modify 28.4.3 [complex], class template complex synopsis, as indicated:

    namespace std {
      template<class T> class complex {
      public:
        using value_type = T;
    
        constexpr complex(T re = T(), T im = T()) requires floating_point<T>;
        constexpr complex(const T& re = T(), const T& im = T()) requires (!floating_point<T>);
        […]
        constexpr complex& operator= (T) requires floating_point<T>;
        constexpr complex& operator= (const T&) requires (!floating_point<T>);
        constexpr complex& operator+=(T) requires floating_point<T>;
        constexpr complex& operator+=(const T&) requires (!floating_point<T>);
        constexpr complex& operator-=(T) requires floating_point<T>;
        constexpr complex& operator-=(const T&) requires (!floating_point<T>);
        constexpr complex& operator*=(T) requires floating_point<T>;
        constexpr complex& operator*=(const T&) requires (!floating_point<T>);
        constexpr complex& operator/=(T) requires floating_point<T>;
        constexpr complex& operator/=(const T&) requires (!floating_point<T>);
        […]
      };
    }
    
  2. Modify 28.4.4 [complex.members] as indicated:

    constexpr complex(T re = T(), T im = T()) requires floating_point<T>;
    constexpr complex(const T& re = T(), const T& im = T()) requires (!floating_point<T>);
    

    -1- Postconditions: real() == re && imag() == im is true.

  3. Modify 28.4.5 [complex.member.ops] as indicated:

    [Drafting note: We have an pre-existing specification hole that the effects of the non-compound assignment operator taking the value_type as parameter are nowhere specified. This is going to be submitted as a separate issue, see LWG 3934.]

    constexpr complex& operator+=(T rhs) requires floating_point<T>;
    constexpr complex& operator+=(const T& rhs) requires (!floating_point<T>);
    

    -1- Effects: Adds the scalar value rhs to the real part of the complex value *this and stores the result in the real part of *this, leaving the imaginary part unchanged.

    -2- Returns: *this.

    constexpr complex& operator-=(T rhs) requires floating_point<T>;
    constexpr complex& operator-=(const T& rhs) requires (!floating_point<T>);
    

    -3- Effects: Subtracts the scalar value rhs from the real part of the complex value *this and stores the result in the real part of *this, leaving the imaginary part unchanged.

    -4- Returns: *this.

    constexpr complex& operator*=(T rhs) requires floating_point<T>;
    constexpr complex& operator*=(const T& rhs) requires (!floating_point<T>);
    

    -5- Effects: Multiplies the scalar value rhs by the complex value *this and stores the result in *this.

    -6- Returns: *this.

    constexpr complex& operator/=(T rhs) requires floating_point<T>;
    constexpr complex& operator/=(const T& rhs) requires (!floating_point<T>);
    

    -7- Effects: Divides the scalar value rhs into the complex value *this and stores the result in *this.

    -8- Returns: *this.