'What problem do C++20 concepts really solve? [duplicate]
I'm trying to understand what problems C++20 concepts are solving and how exactly they are helpful to the end user. I understand that it helps the compiler in resolving function calls, but from my cursory reading of it, from an end user standpoint, it doesn't seem to add much.
Consider this code:
void addToCollection(auto& collection, const auto& val) {
collection.push_back(val);
}
int main() {
vector<int> fooVec;
addToCollection(fooVec, 15);
unordered_set<int> fooSet;
addToCollection(fooSet, 15);
return 0;
}
The compiler issues a very obvious message telling us the function addToCollection is not compatible with unordered_set.
error: no member named 'push_back' in 'std::unordered_set<int>'
collection.push_back(val);
~~~~~~~~~~ ^
concept.cpp:34:5: note: in instantiation of function template specialization 'addToCollection<std::unordered_set<int>, int>'
requested here
addToCollection(fooSet, 15);
Prior to concepts, you'd resolve a failure like this by providing an explicit template specialization.
template<>
void addToCollection(unordered_set<int>& collection, const auto& val) {
collection.insert(val);
}
With concepts, it appears that the correct way to fix this is by placing a constraint on the function parameter type.
template<typename CollectionType>
concept HasPushBack = requires(CollectionType collection, typename CollectionType::value_type val) {
collection.push_back(val);
};
void addToCollection(HasPushBack auto& collection, const auto& val) {
collection.push_back(val);
}
void addToCollection(auto& collection, const auto& val) {
collection.insert(val);
}
As an end user I don't see what advantages I get from concepts in this example—why should I go through the exercise of defining a type constraint when in fact, I could provide an explicit template specialization and be done?
Solution 1:[1]
First, do not mistake a way that a tool can be used for "the purpose" of that tool. You can hammer a nail in with the butt-end of a screwdriver, but that isn't why we make screwdrivers.
Even so, the difference here is that your conceptualized version works on more types. Your specialized version only worked for unordered_set and only for unordered_set<int>. Any other type (unordered_set<float>, set<int>, etc) would fail. The conceptualized version works for any type with a push_back or insert (though I would also check for insert, so that you'd get a better error if the type doesn't have insert).
Second, this example doesn't really exercise concepts. Your code doesn't really define a "concept" here; it's just a test for a single, specific member function.
Concepts as a feature shine when you are building a series of significant requirements, with relationships between them. Iterators and ranges are the go to examples for their utility as a feature.
Before concepts, what can you say about this function declaration:
template<typename InputIterator>
auto &value(InputIterator it);
Well, from the text used, the parameter is expected to be an "InputIterator". But... what does that mean? "InputIterator" is just text; it doesn't mean anything by itself. You'd have to go to the documentation of the function to figure that out.
If you were using dynamic polymorphism with base classes, you could just go to the base class and see what the expected interface needs to be. The requirements of a derived class would be spelled out by the C++ code itself.
Concepts is a tool for doing that for compile-time static polymorphism. It is a way for spelling out the interface requirements of a template using actual C++ code, such that a user can have some idea of what they're expected to provide. It also ensures that the user actually provides that interface.
And it allows you to overload based on these interfaces. Not just function overloading, but class overloading. You can constrain class templates, and their member functions, on concepts.
You can easily make a class template that takes an iterator of any kind and generates an interface matching whatever kind of iterator is given to it. If the given iterator is only an input_iterator, then you can generate just the input_iterator functions. If it's a random_access_iterator, then you can generate those functions too. And while you could do that in pre-C++20 via SFINAE (see here & here) techniques, with concepts, this is about as trivial as it gets.
template<input_iterator It>
class mirror
{
public:
iter_reference_t<It> operator*() const; //All input iterators can do this.
mirror &operator++(); //All input iterators can do this.
mirror operator+(std::iter_difference_t<It>n) requires random_access_iterator<It>; //Only random access iterators get this one.
//etc.
};
Solution 2:[2]
C++20 concepts primarily solve one problem: Error messages a human being can read.
They can make the code more readable too but that is more secondary.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Gabriel Staples |
| Solution 2 | Gabriel Staples |
