#include <iostream>
#include <string>
template<class F>
struct I
{
F _f;
I(F f):_f(f){}
};
template<class F>
struct C
{
F _f;
C(F f):_f(f){}
};
template<class F>
struct S
{
F _f;
std::string _s;
S(std::string const& s,F f):_s(s),_f(f)
{
}
};
C c(std::string("."));
S s1(std::string(",Char: "),c);
I i(s1);
S s2(std::string("Int: "),i);
std::string _sprintf(std::string const& prefix,std::string const& str)
{
return prefix+str;
}
template<class A>
auto _sprintf(std::string const& prefix,I<A> const& a)
{
return [&](int i){ return _sprintf(prefix+std::to_string(i),a._f);};
}
template<class A>
auto _sprintf(std::string const& prefix,C<A> const& a)
{
return [&](char c){return _sprintf(prefix + std::string(1,c),a._f);};
}
template<class A>
auto _sprintf(std::string const& prefix,S<A> const& a)
{
return _sprintf(prefix + a._s,a._f);
}
template<class Fmt>
auto sprintf(Fmt const& fmt)
{
return _sprintf("",fmt);
}
int main()
{
auto test = sprintf(s2);
std::cout<<test(5)('x');
}
I try to create a type safe sprintf. I have taken the example from a Haskell paper and I tried to convert it to C++. The compiler does not complain but at runtime I take
terminate called after throwing an instance of ‘std::bad_alloc’
what(): std::bad_alloc
zsh: IOT instruction (core dumped) ./printf
6
Just because your program compiles doesn’t mean you can’t have logical mistakes. In this case, you have memory errors. Memory errors in C++ can show up far from their source. Tools like address sanitizer can help catch memory problems at their source. In this case, address sanitizer tells the following:
=================================================================
==1==ERROR: AddressSanitizer: stack-use-after-return on address 0x71e077b000b0 at pc 0x71e07a0794b6 bp 0x7ffc765a61f0 sp 0x7ffc765a59b0
READ of size 5 at 0x71e077b000b0 thread T0
#0 0x71e07a0794b5 in memcpy (/opt/compiler-explorer/gcc-14.2.0/lib64/libasan.so.8+0xfa4b5) (BuildId: e522418529ce977df366519db3d02a8fbdfe4494)
#1 0x71e079e6b8c1 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long) (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0x15e8c1) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79)
#2 0x4042b2 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > std::operator+<char, std::char_traits<char>, std::allocator<char> >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&&) /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/bits/basic_string.h:3691
#3 0x4030f0 in _sprintf<S<C<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, I<S<C<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > const&)::{lambda(int)#1}::operator()(int) const /app/example.cpp:42
#4 0x40251c in main /app/example.cpp:65
#5 0x71e079829d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 490fef8403240c91833978d494d39e537409b92e)
#6 0x71e079829e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f) (BuildId: 490fef8403240c91833978d494d39e537409b92e)
#7 0x402334 in _start (/app/output.s+0x402334) (BuildId: 1fd827987fb5ef5739ae395faa6d440683767fc7)
https://godbolt.org/z/EE8sd5cqK
stack-use-after-return is generally a dangling reference problem. The _sprintf("",fmt);
call creates a temporary std::string
that binds to the std::string const& prefix
parameter. The prefix
is captured by reference but that temporary dies at the end of sprintf
, leaving the returned lambda with a dangling reference. To fix this problem you can capture by value:
template<class A>
auto _sprintf(std::string const& prefix,I<A> const& a)
{
return [prefix, &a](int i){ return _sprintf(prefix+std::to_string(i),a._f);};
}
template<class A>
auto _sprintf(std::string const& prefix,C<A> const& a)
{
return [prefix, &a](char c){return _sprintf(prefix + std::string(1,c),a._f);};
}
As a side note: Generally you can and should avoid const std::string&
in C++ and use std::string_view
instead. Similar lifetime care is needed with std::string_view
, still.