I was trying to specialize std::formatter<T>
for a basic custom type Vector3D
, but I'm receiving a compiler error. Here is a minimal reproducible example:
vector.h
defines the Vector3D
class and defines the std::formatter<Vector3D>
specialization, but only declares its format
function:
#ifndef VECTOR_H
#define VECTOR_H
#include <format>
using T = float;
/* Define `Vector3D` */
struct Vector3D {
T x, y, z;
Vector3D() {}
Vector3D(T x_, T y_, T z_) : x{x_}, y{y_}, z{z_} {}
};
/* Declare specialization of `std::formatter` for `Vector3D<T>` */
template <>
struct std::formatter<Vector3D> : public std::formatter<std::string> {
auto format(const Vector3D &item, std::format_context &format_context) const;
};
#endif
vector.cpp
includes vector.h
and defines std::formatter<Vector3D>::format()
:
#include "base/vector.h"
/* Define `format` function in the specialization of `std::formatter` for `Vector3D<T>` */
auto std::formatter<Vector3D>::format(
const Vector3D &item,
std::format_context &format_context
) const {
return std::format_to(format_context.out(), "({}, {}, {})", item.x, item.y, item.z);
}
Finally, main.cpp
attempts to std::format
a value of type Vector3D
:
#include "base/vector.h"
int main()
{
std::format("{}\n", Vector3D(1, 2, 3));
}
However, compiling this gives a huge compiler error:
/main.cpp: In function ‘int main()’:
/main.cpp:5:43: warning: ignoring return value of ‘std::string std::format(format_string<_Args ...>, _Args&& ...) [with _Args = {Vector3D}; string = __cxx11::basic_string<char>; format_string<_Args ...> = basic_format_string<char, Vector3D>]’, declared with attribute ‘nodiscard’ [-Wunused-result]
5 | std::format("{}\n", Vector3D(1, 2, 3));
| ^
In file included from vectors.h:4,
from /main.cpp:1:
/usr/include/c++/13/format:3742:5: note: declared here
3742 | format(format_string<_Args...> __fmt, _Args&&... __args)
| ^~~~~~
/usr/include/c++/13/format: In instantiation of ‘static std::__format::_Arg_store<_Context, _Args>::_Element_t std::__format::_Arg_store<_Context, _Args>::_S_make_elt(_Tp&) [with _Tp = Vector3D; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}; _Element_t = std::__format::_Arg_store<std::basic_format_context<std::__format::_Sink_iter<char>, char>, std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle>::_Element_t]’:
/usr/include/c++/13/format:3281:23: required from ‘std::__format::_Arg_store<_Context, _Args>::_Arg_store(_Tp& ...) [with _Tp = {Vector3D}; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}]’
/usr/include/c++/13/format:3330:14: required from ‘auto std::make_format_args(_Args&& ...) [with _Context = basic_format_context<__format::_Sink_iter<char>, char>; _Args = {Vector3D&}]’
/usr/include/c++/13/format:3743:61: required from ‘std::string std::format(format_string<_Args ...>, _Args&& ...) [with _Args = {Vector3D}; string = __cxx11::basic_string<char>; format_string<_Args ...> = basic_format_string<char, Vector3D>]’
/main.cpp:5:16: required from here
/usr/include/c++/13/format:3270:38: error: no matching function for call to ‘std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(Vector3D&)’
3270 | basic_format_arg<_Context> __arg(__v);
| ^~~~~
/usr/include/c++/13/format:3025:9: note: candidate: ‘template<class _Tp> requires __formattable_with<_Tp, _Context, typename _Context::formatter_type<typename std::remove_const<_Tp>::type>, std::basic_format_parse_context<typename _Context::char_type> > std::basic_format_arg<_Context>::basic_format_arg(_Tp&) [with _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>]’
3025 | basic_format_arg(_Tp& __v) noexcept
| ^~~~~~~~~~~~~~~~
/usr/include/c++/13/format:3025:9: note: template argument deduction/substitution failed:
/usr/include/c++/13/format:3025:9: note: constraints not satisfied
/usr/include/c++/13/format: In substitution of ‘template<class _Tp> requires __formattable_with<_Tp, _Context, typename _Context::formatter_type<typename std::remove_const<_Tp>::type>, std::basic_format_parse_context<typename _Context::char_type> > std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(_Tp&) [with _Tp = std::basic_format_context<std::__format::_Sink_iter<char>, char>]’:
/usr/include/c++/13/format:3270:31: required from ‘static std::__format::_Arg_store<_Context, _Args>::_Element_t std::__format::_Arg_store<_Context, _Args>::_S_make_elt(_Tp&) [with _Tp = Vector3D; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}; _Element_t = std::__format::_Arg_store<std::basic_format_context<std::__format::_Sink_iter<char>, char>, std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle>::_Element_t]’
/usr/include/c++/13/format:3281:23: required from ‘std::__format::_Arg_store<_Context, _Args>::_Arg_store(_Tp& ...) [with _Tp = {Vector3D}; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}]’
/usr/include/c++/13/format:3330:14: required from ‘auto std::make_format_args(_Args&& ...) [with _Context = basic_format_context<__format::_Sink_iter<char>, char>; _Args = {Vector3D&}]’
/usr/include/c++/13/format:3743:61: required from ‘std::string std::format(format_string<_Args ...>, _Args&& ...) [with _Args = {Vector3D}; string = __cxx11::basic_string<char>; format_string<_Args ...> = basic_format_string<char, Vector3D>]’
/main.cpp:5:16: required from here
/usr/include/c++/13/format:2226:13: required for the satisfaction of ‘__formattable_with<_Tp, _Context, typename _Context::formatter_type<typename std::remove_const<_Tp>::type>, std::basic_format_parse_context<typename _Context::char_type> >’ [with _Tp = Vector3D; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>]
/usr/include/c++/13/format:2228:7: in requirements with ‘const _Formatter __cf’, ‘_Tp&& __t’, ‘_Context __fc’ [with _Formatter = std::formatter<Vector3D, char>; _Tp = Vector3D; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>]
/usr/include/c++/13/format:2230:20: note: the required expression ‘__cf.format(__t, __fc)’ is invalid
2230 | { __cf.format(__t, __fc) } -> same_as<typename _Context::iterator>;
| ~~~~~~~~~~~^~~~~~~~~~~
/usr/include/c++/13/format: In instantiation of ‘static std::__format::_Arg_store<_Context, _Args>::_Element_t std::__format::_Arg_store<_Context, _Args>::_S_make_elt(_Tp&) [with _Tp = Vector3D; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}; _Element_t = std::__format::_Arg_store<std::basic_format_context<std::__format::_Sink_iter<char>, char>, std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle>::_Element_t]’:
/usr/include/c++/13/format:3281:23: required from ‘std::__format::_Arg_store<_Context, _Args>::_Arg_store(_Tp& ...) [with _Tp = {Vector3D}; _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>; _Args = {std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::handle}]’
/usr/include/c++/13/format:3330:14: required from ‘auto std::make_format_args(_Args&& ...) [with _Context = basic_format_context<__format::_Sink_iter<char>, char>; _Args = {Vector3D&}]’
/usr/include/c++/13/format:3743:61: required from ‘std::string std::format(format_string<_Args ...>, _Args&& ...) [with _Args = {Vector3D}; string = __cxx11::basic_string<char>; format_string<_Args ...> = basic_format_string<char, Vector3D>]’
/main.cpp:5:16: required from here
cc1plus: note: set ‘-fconcepts-diagnostics-depth=’ to at least 2 for more detail
/usr/include/c++/13/format:2837:7: note: candidate: ‘std::basic_format_arg<_Context>::basic_format_arg() [with _Context = std::basic_format_context<std::__format::_Sink_iter<char>, char>]’
2837 | basic_format_arg() noexcept : _M_type(__format::_Arg_none) { }
| ^~~~~~~~~~~~~~~~
/usr/include/c++/13/format:2837:7: note: candidate expects 0 arguments, 1 provided
/usr/include/c++/13/format:2775:11: note: candidate: ‘constexpr std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(const std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >&)’
2775 | class basic_format_arg
| ^~~~~~~~~~~~~~~~
/usr/include/c++/13/format:2775:11: note: no known conversion for argument 1 from ‘Vector3D’ to ‘const std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >&’
/usr/include/c++/13/format:2775:11: note: candidate: ‘constexpr std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >::basic_format_arg(std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >&&)’
/usr/include/c++/13/format:2775:11: note: no known conversion for argument 1 from ‘Vector3D’ to ‘std::basic_format_arg<std::basic_format_context<std::__format::_Sink_iter<char>, char> >&&’
When I just define the format
function directly in the header file vector.h
, though, everything works. Can someone explain what is going on here?
The issue is with the auto
return type.
When you do #include "base/vector.h"
and then call std::format
, the compiler can only see the declaration of your format
function:
template <>
struct std::formatter<Vector3D> : public std::formatter<std::string> {
auto format(const Vector3D &item, std::format_context &format_context) const;
};
but not the implementation as it is in another translation unit.
The compiler will need to check this function declaration against some constraint. However, since you declared the function with auto
return type, it cannot figure out the return type just by looking at the declaration.
When I just define the format function directly in the header file vector.h, though, everything works.
If you do that then the compiler can see the definition of the function and deduce the return type.
The solution is to either just keep the definition in the header, or use decltype
to get the return type:
template <>
struct std::formatter<Vector3D> : public std::formatter<std::string> {
auto format(const Vector3D &item, std::format_context &format_context) const
-> decltype(format_context.out());
};