C++ 20 Concepts: Require operator overloading - c++

I am trying to understand how to declare a concept that requires a particular operator is overloaded for a given type. Lets say I have the following function that takes a vector of an arbitrary type and prints it to std::cout:
template<typename printable>
void print_vector(const std::vector<printable>& vec)
{
std::cout << '{';
for (const printable &item : vec) {
std::cout << item << ',';
}
std::cout << '}';
}
This code will work just fine if the type printable has an overloaded << operator, but if it doesn't, then it fails with a very unhelpful compiler error. I feel like I should be able to somehow declare a concept that requires a type has a valid << operator defined, and use that concept in the function declaration, so that I can get a more useful compiler error, but I haven't been able to figure out how to do it.

template <class T>
concept Printable = requires(std::ostream& os, T a)
{
os << a;
};
template<Printable T>
void print_vector(const std::vector<T>& vec) {
std::cout << '{';
for (const auto &item : vec) {
std::cout << item << ',';
}
std::cout << '}';
}
If you wish you could also make it more generic to operate on basic_ostream.
Here is the clang error message:
<source>:30:5: error: no matching function for call to 'print_vector'
print_vector(x);
^~~~~~~~~~~~
<source>:19:6: note: candidate template ignored: constraints not satisfied [with T = X]
void print_vector(std::vector<T> vec) {
^
<source>:18:10: note: because 'X' does not satisfy 'Printable'
template<Printable T>
^
<source>:10:9: note: because 'os << a' would be invalid: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'X')
os << a;
^
and gcc:
<source>: In function 'auto test()':
<source>:30:19: error: use of function 'void print_vector(std::vector<T>) [with T = X]' with unsatisfied constraints
30 | print_vector(x);
| ^
<source>:19:6: note: declared here
19 | void print_vector(std::vector<T> vec) {
| ^~~~~~~~~~~~
<source>:19:6: note: constraints not satisfied
<source>: In instantiation of 'void print_vector(std::vector<T>) [with T = X]':
<source>:30:19: required from here
<source>:8:9: required for the satisfaction of 'Printable<T>' [with T = X]
<source>:8:21: in requirements with 'std::ostream& os', 'T a' [with T = X]
<source>:10:9: note: the required expression '(os << a)' is invalid
10 | os << a;
| ~~~^~~~
cc1plus: note: set '-fconcepts-diagnostics-depth=' to at least 2 for more detail

Related

Implicit conversion of initializer lists and perfect forwarding

I'm trying to make perfect forwarding work with initializer lists. For the sake of the example, I'd like to have a variadic function that calls into another function, and still enjoy automatic conversion of initializer lists of the latter:
#include <iostream>
#include <vector>
void hello(std::string const& text, std::vector<int> const& test)
{
std::cout << "hello " << text << " " << test.size() << std::endl;
}
template<class ... Args>
void f(Args&& ... args)
{
return hello(std::forward<Args>(args)...);
}
int main()
{
hello("world", {1,2,3}); // WORKS
f("world", std::vector<int>({1,2,3})); // WORKS
f("world", {1,2,3}); // COMPILER ERROR
}
The error is
example.cpp: In function ‘int main()’:
example.cpp:21:21: error: too many arguments to function ‘void f(Args&& ...) [with Args = {}]’
21 | f("world", {1,2,3});
| ^
example.cpp:12:6: note: declared here
12 | void f(Args&& ... args)
| ^
example.cpp: In instantiation of ‘void f(Args&& ...) [with Args = {}]’:
example.cpp:21:21: required from here
example.cpp:14:15: error: too few arguments to function ‘void hello(const string&, const std::vector<int>&)’
14 | return hello(std::forward<Args>(args)...);
| ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
example.cpp:6:6: note: declared here
6 | void hello(std::string const& text, std::vector<int> const& test)
| ^~~~~
Am I making any obvious mistake here?
The compiler is not able to recognize the type you are sending in the third case.
If you use
f("world", std::initializer_list<int>{1,2,3});
everything works.
This post has some detailed explanation and quotes the relevant part of the standard. It is for a slightly different case but the explanation still applies.
The problem is that the {1, 2, 3} argument to your second call to the templated f function is not sufficiently 'specific' for the compiler to unambiguously deduce its type in template substitution.
Explicitly defining that argument's type will resolve the issue:
f("world", std::initializer_list<int>{ 1, 2, 3 });
A very similar case is given (as an example of an error) on this cppreference page.

Extended copy constructor with custom allocators

In my experiments with scoped_allocator_adaptors using x86_64 gcc/clang trunk, I've run into an issue where the code snippet below compiles if the extended copy constructor is enabled but without it results is_constructible static assertion below. The compiler complains of the extended copy constructor not being defined even though it is never invoked when enabled. Can someone help answer why that might be the case?
#include <iostream>
#include <vector>
#include <scoped_allocator>
template <typename T>
struct MyAlloc
{
using value_type = T;
MyAlloc(const std::string &scope) noexcept : _scope(scope) {}
// Rebinding allocatos to different type
template <class U> MyAlloc(MyAlloc<U> const& other) noexcept : _scope(other._scope) {}
value_type* allocate(std::size_t n) noexcept
{
std::cout << "Allocating " << n << " objects within " << _scope << " from " << __PRETTY_FUNCTION__ << std::endl;
return static_cast<value_type*>(::operator new (n*sizeof(value_type)));
}
void deallocate(value_type* p, std::size_t n) noexcept
{
std::cout << "Deallocating " << n << " objects within " << _scope << " from " << __PRETTY_FUNCTION__ << std::endl;
::operator delete(p);
}
std::string _scope;
};
template <typename T>
using MyAllocAdaptor = std::scoped_allocator_adaptor<MyAlloc<T>>;
template <typename T> // adaptor to propagate
using myvec = std::vector<T, MyAllocAdaptor<T>>;
template <typename T>
using bstr = std::basic_string<T, std::char_traits<T>, MyAlloc<T>>;
using mystring = bstr<char>;
// Example struct with multiple nested containers with different types
// More realistic example that single type containers
class S
{
int z;
mystring str;
myvec<int> vec;
public:
// If not public, the allocator aware constructor is not invoked
// limiting the propagation of the allocator
using allocator_type = MyAllocAdaptor<S>;
S(allocator_type alloc) :
z(1),
str("This string should really not have SBO....", std::allocator_traits<allocator_type>::rebind_alloc<char>(alloc)),
vec(std::allocator_traits<allocator_type>::rebind_alloc<int>(alloc))
{
vec.push_back(10);
}
S() : S(allocator_type("NoScope")) {
std::cout << "Should not be invoked " << __PRETTY_FUNCTION__ << std::endl;
}
~S()
{
std::cout << __PRETTY_FUNCTION__ << " Z " << z << std::endl;
}
#if 0 // If this is defined things compile
S(const S& other, allocator_type alloc = {}) :
str(other.str, std::allocator_traits<allocator_type>::rebind_alloc<char>(alloc)),
vec(other.vec, std::allocator_traits<allocator_type>::rebind_alloc<int>(alloc))
{
}
#endif
};
int main()
{
MyAlloc<S> alloc("scope1");
myvec<S> vec(alloc);
vec.emplace_back();
}
Compiler error when the extended copy constructor is not defined (even though it's never invoked when defined)
In file included from <source>:3:
In file included from /opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/11.0.0/../../../../include/c++/11.0.0/scoped_allocator:39:
In file included from /opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/11.0.0/../../../../include/c++/11.0.0/tuple:40:
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/11.0.0/../../../../include/c++/11.0.0/bits/uses_allocator.h:96:7: error: static_assert failed due to requirement '__or_<std::is_constructible<S, std::allocator_arg_t, const std::scoped_allocator_adaptor<MyAlloc<S>> &, const S &>, std::is_constructible<S, const S &, const std::scoped_allocator_adaptor<MyAlloc<S>> &>>::value' "construction with an allocator must be possible if uses_allocator is true"
static_assert(__or_<
^ ~~~~~~
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/11.0.0/../../../../include/c++/11.0.0/scoped_allocator:377:8: note: in instantiation of template class 'std::__uses_alloc<true, S, std::scoped_allocator_adaptor<MyAlloc<S>>, const S &>' requested here
= std::__use_alloc<_Tp, inner_allocator_type, _Args...>(__inner);
^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/11.0.0/../../../../include/c++/11.0.0/bits/alloc_traits.h:247:8: note: in instantiation of function template specialization 'std::scoped_allocator_adaptor<MyAlloc<S>>::construct<S, const S &>' requested here
{ __a.construct(__p, std::forward<_Args>(__args)...); }
^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/11.0.0/../../../../include/c++/11.0.0/bits/stl_uninitialized.h:318:16: note: in instantiation of function template specialization 'std::allocator_traits<std::scoped_allocator_adaptor<MyAlloc<S>>>::construct<S, const S &>' requested here
__traits::construct(__alloc, std::__addressof(*__cur), *__first);
^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/11.0.0/../../../../include/c++/11.0.0/bits/stl_uninitialized.h:353:19: note: in instantiation of function template specialization 'std::__uninitialized_copy_a<const S *, S *, std::scoped_allocator_adaptor<MyAlloc<S>>>' requested here
return std::__uninitialized_copy_a
^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/11.0.0/../../../../include/c++/11.0.0/bits/vector.tcc:473:10: note: in instantiation of function template specialization 'std::__uninitialized_move_if_noexcept_a<S *, S *, std::scoped_allocator_adaptor<MyAlloc<S>>>' requested here
= std::__uninitialized_move_if_noexcept_a
^
/opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/11.0.0/../../../../include/c++/11.0.0/bits/vector.tcc:121:4: note: in instantiation of function template specialization 'std::vector<S, std::scoped_allocator_adaptor<MyAlloc<S>>>::_M_realloc_insert<>' requested here
_M_realloc_insert(end(), std::forward<_Args>(__args)...);
^
<source>:78:9: note: in instantiation of function template specialization 'std::vector<S, std::scoped_allocator_adaptor<MyAlloc<S>>>::emplace_back<>' requested here
vec.emplace_back();
^
1 error generated.
Compiler returned: 1

Why can't the compiler use the std::string conversion function of the class when perform operator<<?

Consider the following struct with a user-defined conversion function that can convert itself to const char*;
struct S {
operator const char*() { return "hello"; }
};
This work with <iostream>, we can print the struct S with no error message:
std::cout << S{} << '\n';
But if I change the return type to std::string:
struct S {
operator std::string() { return "hello"; }
};
I got this compiler error message:
<source>:11:13: error: no match for 'operator<<' (operand types are 'std::ostream' {aka 'std::basic_ostream<char>'} and 'S')
11 | std::cout << S{} << '\n';
| ~~~~~~~~~ ^~ ~~~
| | |
| | S
| std::ostream {aka std::basic_ostream<char>}
<source>:11:18: note: 'S' is not derived from 'const std::__cxx11::basic_string<_CharT, _Traits, _Allocator>'
11 | std::cout << S{} << '\n';
| ^
Why can't the compiler use the std::string conversion? Is there a difference between the conversion function of the built-in and class type?
Because operator<< for std::basic_string is a template taking 3 template parameters:
template <class CharT, class Traits, class Allocator>
std::basic_ostream<CharT, Traits>&
operator<<(std::basic_ostream<CharT, Traits>& os,
const std::basic_string<CharT, Traits, Allocator>& str);
And implicit conversion won't be considered in template argument deduction:
Type deduction does not consider implicit conversions (other than type adjustments listed above): that's the job for overload resolution, which happens later.
Then given std::cout << S{};, the template parameter CharT, Traits and Allocator can't be deduced on the 2nd function argument.
On the other hand, operator<< for const char* doesn't have such issue; given std::cout << S{};, the template parameter CharT and Traits would be deduced only from the 1st function argument. After deduction, the implicit conversion from S to const char* will be performed and the calling works well.

auto-returning functions and template instantiation

While writing some template code, I ran into <unresolved overloaded function type> errors which can be reduced to the following.
template <int N>
auto bar()
{
return N;
}
int main(int, char* [])
{
auto foo = [] (auto func) {
return func();
};
foo(bar<3>);
}
With the errors being:
unresolved_overload.cpp: In function 'int main(int, char**)':
unresolved_overload.cpp:26:28: error: no match for call to '(main(int, char**)::<lambda(auto:1)>) (<unresolved overloaded function type>)'
std::cout << foo(bar<3>) << std::endl;
^
unresolved_overload.cpp:21:29: note: candidate: template<class auto:1> constexpr main(int, char**)::<lambda(auto:1)>::operator decltype (((const main(int, char**)::<lambda(auto:1)>*)((const main(int, char**)::<lambda(auto:1)>* const)0))->operator()(static_cast<auto:1&&>(<anonymous>))) (*)(auto:1)() const
auto foo = [] (auto func) {
^
unresolved_overload.cpp:21:29: note: template argument deduction/substitution failed:
unresolved_overload.cpp:26:28: note: couldn't deduce template parameter 'auto:1'
std::cout << foo(bar<3>) << std::endl;
^
unresolved_overload.cpp:21:29: note: candidate: template<class auto:1> main(int, char**)::<lambda(auto:1)>
auto foo = [] (auto func) {
^
unresolved_overload.cpp:21:29: note: template argument deduction/substitution failed:
unresolved_overload.cpp:26:28: note: couldn't deduce template parameter 'auto:1'
std::cout << foo(bar<3>) << std::endl;
If we replace the auto-return with the explicit return type, int, the example will compile fine.
Why does auto-return run into these issues? I looked into template argument deduction and substitution but the search was largely unfruitful. I thought it might have something to do with the order of template instantiation / etc but couldn't make too much sense of it...
Per AndyG's suggestion, I found the same issue on GCC's bug list. Bug 64194. First reported in 2014. Thus the conclusion seems to be that this is a GCC bug and thankfully not another special case for templates.
Working around this just requires having something else to trigger the instantiation (e.g. assign to a variable, a using declaration).
Try this:
template <typename func>
auto bar(func&& f)->decltype(f())
{
return f();
}
int main()
{
int i = 100;
auto f = [=]()
{
return i;
};
bar(f);
return 0;
}

How to use enable_if to conditionally define a << operator

I'm trying to define a << operator for a set of classes; the set is open, but all of the members have a common tagging base class, and all have the member function std::string String() const. Basically, what I've got is:
class Tag {};
class Obj : public Tag
{
public:
std::string String() const { return "specialized"; }
};
template <typename T>
typename std::enable_if<std::is_base_of<Tag, T>::type, std::ostream>::value& operator<<( std::ostream& dest, T const& source)
{
dest << source.String();
return dest;
}
int
main()
{
std::cout << typeid(std::enable_if<std::is_base_of<Tag, Obj>::value, std::ostream>::type).name() << std::endl;
std::string s( "generic" );
Obj e;
std::cout << e << std::endl;
std::cout << s << std::endl;
return 0;
}
This doesn't work: with g++ (version 4.8.3, invoked with -std=c++11), I get the error message:
enableIf.cc: In function 'int main()':
enableIf.cc:55:18: error: cannot bind 'std::ostream {aka std::basic_ostream<char>}' lvalue to 'std::basic_ostream<char>&&'
std::cout << e << std::endl;
^
In file included from /usr/lib/gcc/x86_64-pc-cygwin/4.8.3/include/c++/iostream:39:0,
from enableIf.cc:8:
/usr/lib/gcc/x86_64-pc-cygwin/4.8.3/include/c++/ostream:602:5: error: initializing argument 1 of 'std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&&, const _Tp&) [with _CharT = char; _Traits = std::char_traits<char>; _Tp = Obj]'
operator<<(basic_ostream<_CharT, _Traits>&& __os, const _Tp& __x)
^
I can't figure it out, because there aren't any rvalue-references in sight; the compiler seems to have struck on the generic overload for std::ostream&& in the standard library.
With MSC (VS 2013), the error message is a lot more verbose, but it starts with:
enableIf.cc(55) : error C2679: binary '<<' : no operator found which takes a right-hand operand of type 'Obj' (or there is no acceptable conversion)
and then goes on to list a lot of possible functions, all in the standard library.
(In my actual code, line 55 corresponds to the line std::cout << e << std::endl;.)
In both cases, the compiler seems to be rejecting my overloaded function. If I comment out the << lines, however, the code compiles, and the value output by the first line in main seems correct (at least with MSC—the output of g++ is So, what ever that's supposed to mean).
Given that two compilers agree, I assume that there is an error in my code, but I can't figure out what. How do you do this? (FWIW: I'd be equally happy, or even happier, with a solution which generates the overload for all types having a member function std::string Type::String() const.)
I'm pretty sure you meant this:
template <typename T> // here here
typename std::enable_if<std::is_base_of<Tag, T>::type, std::ostream>::value&
operator<<( std::ostream& dest, T const& source)
to be this:
template <typename T>
typename std::enable_if<std::is_base_of<Tag, T>::value, std::ostream>::type&
operator<<( std::ostream& dest, T const& source)
after changing as such, you compile successfully.