3

Say I'm writing a custom container and want to provide a RandomAccessIterator for it.

template<class T>
class MyContainer
{
public:
    class RandomAccessIterator
    {
         /*
             ... operators, constructors, etc...
         */
    };
    RandomAccessIterator begin();
    RandomAccessIterator end();

/*
    ... class implementation ...
*/
};

Now, how can I check that the iterator I've just written is 100% compatible with standard library algorithms? One obvious solution is to simply try one:

MyContainer<int> list;
std::sort(list.begin(), list.end());

...and verify whether it compiles and produces desired results.

With that in mind, I am not sure if it's a reliable "trait compliance" test and that's exactly what this question is about. Will the compiler always complain if I forget to provide any of the required methods/operators? If not, what would be the best way to make sure that my types fully implement given traits?

@Edit: the post no longer refers to the standard library as to "the STL", which was incorrect as pointed out in the comments.

mdx
  • 534
  • 3
  • 16
  • You might want to check e.g. [this iterator category reference](https://en.cppreference.com/w/cpp/iterator#Iterator_categories). If you implement all operators for a specific category then your iterator is of that category and can be used whenever the standard library expects such an iterator. – Some programmer dude Dec 23 '19 at 18:11
  • Regarding [`std::sort`](https://en.cppreference.com/w/cpp/algorithm/sort) it requires that the iterators are [value swappable](https://en.cppreference.com/w/cpp/named_req/ValueSwappable) and are [(legacy) random access iterators](https://en.cppreference.com/w/cpp/named_req/RandomAccessIterator). – Some programmer dude Dec 23 '19 at 18:12
  • 4
    You can't statically check that your iterator is compatible because iterator categories describe expected behaviors, not just interfaces. So your implementation has to not only provide the right interface but it has to do the right thing as well. – François Andrieux Dec 23 '19 at 18:17
  • 1
    [What's the difference between “STL” and “C++ Standard Library”?](https://stackoverflow.com/questions/5205491/whats-the-difference-between-stl-and-c-standard-library) – François Andrieux Dec 23 '19 at 18:17
  • *If you implement all operators for a specific category then your iterator is of that category and can be used whenever the standard library expects such an iterator.* I realize that. But is it **guaranteed** for the compilation to fail even if the functions I don't provide are not used by the algorithm I'm testing against? – mdx Dec 23 '19 at 18:17
  • 1
    "Is it guaranteed" - No. – Evg Dec 23 '19 at 18:28

2 Answers2

2

I would say it is possible based on C++20 iterator concepts1:

template<std::input_iterator T>
using validate_input_iterator = T;

This would fail compilation:

struct meow {
    struct iterator{};
private:
    typedef validate_input_iterator<iterator> v; // fails here
};

However the below compiles:

struct meow: private std::string { // string has an input iterator
    // struct iterator{};
private:
    typedef validate_input_iterator<iterator> v; // passes!
};

https://godbolt.org/z/5DwiaT

If you actually want to check you implemented the required functions and traits correctly, this would of course couldn't be checked at compile time and requires writing some tests.


Syntactic requirements vs. Semantic requirements

Note that concepts, and specifically iterator concepts, declare both syntactic requirements and semantic requirements. While the syntactic requirements, stated based on the requires syntax, can be statically checked at compile time, the semantic requirements that are merely "notes" to be followed by that type, are not and cannot be checked at compile time. On the other hand algorithms and compiler decisions may be based on the fact that a type said to implement a specific concept follows its semantic requirements.

For example, bidirectional_­iterator is defined as:

23.3.4.12 Concept bidirectional_­iterator [iterator.concept.bidir]

  1. The bidirectional_­iterator concept adds the ability to move an iterator backward as well as forward.
     template<class I>
       concept bidirectional_iterator =
         forward_iterator<I> &&
         derived_from<ITER_CONCEPT(I), bidirectional_iterator_tag> &&
         requires(I i) {
           { --i } -> same_as<I&>;
           { i-- } -> same_as<I>;
         };
  1. A bidirectional iterator r is decrementable if and only if there exists some q such that ++q == r. Decrementable iterators r shall be in the domain of the expressions --r and r--.

  2. Let a and b be equal objects of type I. I models bidirectional_­iterator only if:

    (3.1) If a and b are decrementable, then all of the following are true:

    (3.1.1) addressof(--a) == addressof(a)

    (3.1.2) bool(a-- == b)

    (3.1.3) after evaluating both a-- and --b, bool(a == b) is still true

    (3.1.4) bool(++(--a) == b)

    (3.2) If a and b are incrementable, then bool(--(++a) == b).

For a given iterator said to implement bidirectional_­iterator as seen above, bullet #1 can be checked at compile time while bullets #2 and #3 cannot.


1 Should also be doable pre C++20 implementing your own SFINAE restricted template in a similar manner

Amir Kirsh
  • 12,564
  • 41
  • 74
1

I am not sure if it's a reliable "trait compliance" test

Checking if your iterator works with std::sort is not a reliable test of the iterator. The implementation of std::sort will depend on some (unknown) subset of the requirements imposed by the standard, but not on all.

Will the compiler always complain if I forget to provide any of the required methods/operators?

No, it will not. It might even not complain with one version of the standard library but with another version / implementation; if the code you're using your iterator with has changed and depends on different requirements imposed by the standard.

If not, what would be the best way to make sure that my types fully implement given traits?

You go through the C++ standard and note down each and every behavior it describes about the iterator category you want to implement. Then you devise test cases for each of these requirements which - ideally - rely only on that one requirement.

Finally you test your iterator as well as some standard iterators (e.g. from std::vector) with these test cases. Ideally using multiple different implementations and versions of the standard library.

Last but not least it would be awesome if you'd made this work open source so that others can benefit from it (and contribute to it).

Daniel Jour
  • 15,896
  • 2
  • 36
  • 63
  • Thank you for this answer, it clarified much in a clear and concise manner. Frankly, I was hoping for some kind of a silver bullet to exist, but knowing there isn't one is a useful piece of information, too. – mdx Dec 24 '19 at 11:58