5

Beware CTAD on `reverse_iterator` – Arthur O'Dwyer – Stuff mostly about C++

 2 years ago
source link: https://quuxplusone.github.io/blog/2022/08/02/reverse-iterator-ctad/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Beware CTAD on reverse_iterator

Consider the following example of an “STL-style algorithm,” taken from a lab exercise in one of my training courses:

template<class It>
bool is_palindrome(It first, It last) {
    while (first != last) {
        --last;
        if (first == last) break;
        if (*first != *last) {
            return false;
        }
        ++first;
    }
    return true;
}

A palindrome is equal to itself when read forwards or backwards, so, I said blithely, we could rewrite it like this:

template<class It>
bool is_palindrome(It first, It last) {
    return std::equal(
        first, last,
        std::reverse_iterator(last),
        std::reverse_iterator(first)
    );
}

But look out — this code has a bug that causes undefined behavior! Do you see it?


Here’s a test case that triggers the undefined behavior (Godbolt):

std::string s = "foo";
assert(!is_palindrome(s.begin(), s.end()));
assert(is_palindrome(s.begin()+1, s.end()));
assert(!is_palindrome(s.rbegin(), s.rend()));
assert(is_palindrome(s.rbegin(), s.rend()-1));  // Fails! Oof!

The bug is that I left the five characters make_ out of std::make_reverse_iterator. (Or, alternatively, I left the four characters <It> out of std::reverse_iterator<It>.) When I wrote std::reverse_iterator(last) without angle brackets, I was accidentally triggering C++17 class template argument deduction (CTAD). CTAD tries to deduce the template arguments to std::reverse_iterator; and in the latter two cases above, it sees that last is already a reverse_iterator (specifically reverse_iterator<string::iterator>). So CTAD thinks it doesn’t need to do anything.

Thus, in the latter two cases above, std::reverse_iterator(last) is treated as just another way of writing last, and so we’re actually executing std::equal(first, last, last, first) — which immediately runs off the end of the string and has undefined behavior.

My recommended fix is never to use CTAD (certainly never in generic code). I continue to wish that any mainstream compiler would add an opt-in -Wctad diagnostic; see Contra CTAD” (2018-12-09) and “Measuring adoption of C++17 and CTAD in real codebases” (2019-01-16). (I submitted such a diagnostic to Clang back in November 2018, but it never made progress.)


The specific case of reverse_iterator has come up for others in the past:


Separately, it’s mildly interesting that this is an algorithm whose performance can be improved (at least in debug mode) by not using the C++14 four-argument version of std::equal.

template<class It>
bool is_palindrome(It first, It last) {
    return std::equal(
        first, last,
        std::reverse_iterator<It>(last)
        // do NOT pass the end of the second range
    );
}

The four-iterator version can be O(1) in the case that all the iterators are random-access and (last1 - first1) != (last2 - first2): different-length ranges can never be “equal.” But in this case we know our ranges are definitely the same length, and so we can save a bit of time by skipping that comparison.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK