Since there's currently a bounty asking what's new in this area in C++20... The answer is "not much."
The STL Way Of Doing Things remains static polymorphism, not inheritance. The STL doesn't provide any kind of "BaseContainer" for your new container to derive from. Instead, it specifies a bunch of syntactic and semantic requirements, which your new container must conform to.
The classical-OOP-inheritance approach is:
struct BaseContainer { virtual void push_back(Object) = 0; virtual BaseIterator begin() = 0; virtual BaseIterator end() = 0; virtual ~BaseContainer() = default; }; void sort(BaseContainer& ctr) { ~~~~ ctr.begin(); ~~~~ }
But that's not what the STL does. The STL uses templates:
// NOTA BENE: To instantiate this `sort` template, you must pass it // a `ctr` such that `ctr.begin()` and `ctr.end()` are well-formed // expressions that return iterators, and `ctr.end()` is reachable // by repeatedly incrementing from `ctr.begin()`. ~~~~ template<class Container> void sort(Container& ctr) { ~~~~ auto it = ctr.begin(); ~~~~ }
Starting in C++20, the language does provide a nice syntax for encoding part of that comment into compiler-readable C++. The syntax is called a requires-expression.
template<class Container> void sort(Container& ctr) requires requires { { ctr.begin() }; { ctr.end() }; } { ~~~~ auto it = ctr.begin(); ~~~~ }
This encodes the syntactic requirement that ctr.begin() and ctr.end() must both be well-formed expressions. (Of course, that requirement was already encoded implicitly in the body of the function template. If you passed a ctr such that ctr.begin() wasn't well-formed, the function would refuse to compile. But now it refuses even to participate in overload resolution, which is, uh, different.)
There remains no way to encode semantic requirements in C++ — obviously, as the syntax of a programming language can only ever encode syntactic requirements. (The history of programming languages is the history of figuring out how to constrain things such that semantics becomes expressible in syntax.)
Pre-C++20 (and also post-C++20), C++ specifies the notion of "iterator requirements." For example, in order for it to meet the iterator requirements, it must be the case that
++it;
is a well-formed expression and returns a reference to it itself. The former is a syntactic requirement; the latter is a semantic requirement.
(Nit: Technically it's legal for ++it to return a reference to some other iterator, instead of to it, and it'll still meet the Cpp17InputIterator requirements (although not the Cpp17ForwardIterator requirements). I don't know if that's an oversight or what.)
Starting in C++20, the syntactic requirements are formalized in named concepts such as std::input_iterator and std::forward_iterator. The semantic requirements aren't expressed in those concepts (being semantic, they can't be), but the Standard does, on paper, associate a list of semantic requirements with each named concept, and basically says that if your type satisfies the syntactic requirements of a named concept without also modeling the semantic requirements, then all bets are off: the library is allowed to assume that any type satisfying input_iterator also models the semantic requirements of input_iterator, and so on.
So it is reasonably correct to say that C++20 changes the game with respect to writing your own iterator types. You can write your type like
struct MyIterator { ~~~~ };
and then add a simple assertion at the end of the class definition:
static_assert(std::forward_iterator<MyIterator>);
and if that assertion passes, then you know your type is "iterator-ish enough" for every piece of client code in the STL.
(Of course in practice that's not true at all. You can write an iterator that works with C++20 ranges::sort but fails with C++17 std::sort, or vice versa. You can write an iterator that satisfies std::forward_iterator's syntactic requirements but accidentally fails to model the semantic requirements — like, maybe you reversed the sense of its operator==. And so on. But still, C++20's named concepts are really a big step forward compared to what C++17 gave you.)
But: C++20 doesn't add named concepts for "containers" at all! The situation for containers in C++20 (and C++23/26) is still the status quo ante: there's a big table of "container requirements" in the Standard, but there is no named concept (say, "std::container") that encodes all of those requirements into a requires-expression. Nor, really, could there be — because different containers (unlike different iterators) support different operations. vector lacks push_front. deque lacks capacity. forward_list even lacks size!
So if you want to write a concept expressing the syntactic requirements of a "container," you'll have to do it yourself — and it still probably won't capture everything that's the same about all STL containers. Let alone the semantics, which are impossible to capture in code, and would have to be expressed through documentation. (For example, "After calling v.clear(), the value of v.size() must be zero.")
TL;DR — the answer to your question hasn't changed in the past ten years. That answer is that you don't derive from some kind of ContainerBase; instead, you write your own type from scratch, and then manually compare its functionality against the Standard's paper list of container requirements to see if you've correctly modeled them all.
But if you just want to check that your type correctly satisfies the syntactic requirements of a C++20 range, that turns out to be easy, because there's a named concept for that:
static_assert(std::ranges::range<MyContainerType>);
if(is_iterator<T>::value) { /* do something with iterators */ }.