https://gcc.gnu.org/bugzilla/show_bug.cgi?id=115444

            Bug ID: 115444
           Summary: std::copy_n generates more code than needed
           Product: gcc
           Version: unknown
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: arthur.j.odwyer at gmail dot com
  Target Milestone: ---

// https://godbolt.org/z/1Yo3h3rqW
#include <algorithm>
#include <compare>

struct It {
    explicit It();
    It& operator++();
    It operator++(int);
    It& operator--();
    It operator--(int);
    const int& operator*() const;
    const int& operator[](int) const;
    friend It operator+(int, const It&);
    friend It operator+(const It&, int);
    friend It operator-(const It&, int);
    int operator-(const It&) const;
    It& operator+=(int);
    It& operator-=(int);
    bool operator==(const It&) const;
    std::strong_ordering operator<=>(const It&) const;
    using difference_type = int;
    using value_type = int;
    using reference = int&;
    using pointer = int*;
#ifdef LESS
    using iterator_category = std::bidirectional_iterator_tag;
#endif
};

void simple_copy(It first, int n, int *dest) {
    std::copy_n(first, n, dest);
}

Compiled with `-std=c++20 -DLESS`, this produces the codegen I'd expect: a
single loop calling `It::operator++()` and `It::operator*()` and no other
user-defined operations. That is, we pass in the `n`, it loops `n` times, we're
done. This is perfect.

But if you compile with `-std=c++20 -ULESS`, you get more verbose codegen:
first a call to `It::operator+(int)`, then a call to `It::operator-(It)`, and
then the same loop as before. That is, it appears that libstdc++ is "lowering"
`std::copy_n(first, n, dest)` into `std::copy(first, first+n, dest)` and then
"lowering" that back into `std::copy_n(first, first+n - first, dest)` before
finally getting to the loop. It would be nice not to do this.

This was reported on Slack by Alfredo Correa, whose iterators allegedly have
expensive (still O(1), but expensive) `+` and `-` operations.



Sidebar: Alfredo also reports that he's trying to use `copy_n` with iterators
where maybe `first + n` isn't even computable — like, he's expecting that the
following program should have well-defined behavior.

// https://godbolt.org/z/zfq36Ker6
struct Dynamite {
    int value_;
    void operator=(Dynamite d) {
        if (d.value_ == -1) throw "Success!";
        value_ = d.value_;
    }
};

int main() {
    const Dynamite s[4] = {{1},{2},{3},{-1}};
    Dynamite d[4] = {};
    try {
        std::copy_n(s, 1000, d);
        assert(false);
    } catch (const char *s) {
        assert(d[0].value_ == 1);
        assert(d[1].value_ == 2);
        assert(d[2].value_ == 3);
        puts(s);
    }
}

Today, this program forms the invalid pointer value (s+1000) inside libstdc++'s
`std::copy_n`. I think it's within its rights to do that, and thus the behavior
of this program is undefined by the standard (specifically,
https://eel.is/c++draft/alg.copy#12 doesn't claim that it *is* defined, so it's
undefined). But I'd still encourage libstdc++ to make programs like this Just
Work.

Reply via email to