在 gcc 上自己的元组实现段错误,同时在 clang 中工作

own tuple implementation segfaults on gcc while works in clang

本文关键字:工作 clang 错误 gcc 自己的 元组 段错误 实现      更新时间:2023-10-16

作为练习,我正在尝试为 C++17 提出一个元组实现。显然,有一些可疑的东西,因为 Clang 能够正常工作,我猜,但 GCC 段错误。首先是 MVCE:

#include <utility>
#include <string>
#include <type_traits>
#include <iostream>
namespace cho {
template<std::size_t N, typename T, typename... types>
struct get_Nth_type
{
using type = typename get_Nth_type<N - 1, types...>::type;
};
template<typename T, typename... types>
struct get_Nth_type<0, T, types...>
{
using type = T;
};
template<std::size_t N, typename... Args>
using get_type = typename get_Nth_type<N, Args...>::type;
template <std::size_t I, typename T>
struct tuple_leaf {
static auto constexpr ix = I;
T elem;
};
template<class Seq, class... Ts> struct tuple_impl;
template<size_t... Ix, class... Ts>
struct tuple_impl<std::index_sequence<Ix...>, Ts...> : tuple_leaf<Ix, Ts>... { };
template<typename... Ts>
struct tuple : tuple_impl<std::make_index_sequence<sizeof...(Ts)>, Ts...> {};
template <std::size_t I, typename... Ts>
constexpr auto& get(tuple<Ts...> const& t) {
using T = get_type<I, Ts...>;
return static_cast<tuple_leaf<I, T>&>(const_cast<tuple<Ts...>&>(t)).elem;
}
template <std::size_t I, typename Ts>
struct tuple_element;
template <std::size_t I, typename... Ts>
struct tuple_element<I, tuple<Ts...>> {
using type = get_type<I, Ts...>;
};
template <std::size_t I, typename Tuple>
using tuple_element_t = typename tuple_element<I, Tuple>::type;
template <typename... Args>
constexpr auto make_tuple(Args&&... args) {
return tuple<Args...>{std::forward<Args>(args)...};
}
template <typename Tuple, typename T, std::size_t... Ix>
auto constexpr pb_impl(Tuple const& t, T v, std::index_sequence<Ix...> const&) {
return cho::make_tuple(((void)(0), get<Ix>(t))..., v);
}
template <typename T, typename... Ts>
auto constexpr push_back(tuple<Ts...> const& t, T const& v) {
return pb_impl(t, std::move(v), std::make_index_sequence<sizeof...(Ts)>());
}
}
int main() {
auto spt = cho::push_back(cho::make_tuple(42, 3.14f), std::string("e"));
std::cout << cho::get<0>(spt) << "n";
cho::get<2>(spt) = std::string("pi");
static_assert(std::is_same_v<cho::tuple_element_t<0, decltype(spt)>, int&>);
auto st2 = cho::make_tuple(42, 3.14f);
static_assert(std::is_same_v<cho::tuple_element_t<0, decltype(st2)>, int>);
auto spt2 = cho::push_back(st2, std::string("e"));
cho::get<2>(spt2) = std::string("pi");
} 

我还要说,叮当有时也会有段错误。所以,简而言之,这段代码有一些非常糟糕的地方。罪魁祸首是元组push_back实现。我想返回一个新元组,其中包含旧元组的副本和添加到末尾的一个元素。如果你看一下第一个元组示例,我尝试打印第一个元素,我相信这就是我得到段错误的地方。

pb_impl通过为每个元素调用get来调用make_tuple,这将返回对旧元组元素的引用。如果您尝试像我一样使用临时文件,这将有问题:

auto spt = cho::push_back(cho::make_tuple(42, 3.14f), std::string("e"));

在这里,push_back将获得一个临时的元组来处理,并将引用复制到生成的元组。然后spt将保存对已破坏内存部分的引用。我在这里做错了什么?如果我传递了一个临时元组,如何在pb_impl中复制元组的内容?

而且,我不确定您是否能够复制,但是,为什么 Clang 大部分时间都能正常工作,打印 42 没有任何段错误?

请注意,更改此行:

return cho::make_tuple(((void)(0), get<Ix>(t))..., v);

对此

return cho::make_tuple(((void)(0), get<Ix>(t))..., std::move(v));

解决了问题,但我不明白为什么,也不知道这是否是偶然的。

这是罪魁祸首:

template <typename... Args>
constexpr auto make_tuple(Args&&... args) {
return tuple<Args...>{std::forward<Args>(args)...};
}

您要返回的元组由传递给make_tuple的类型组成,但这些是引用。你不想要这个。您需要制作实际值的元组,并使用类似tie函数来制作引用元组。

这是唯一理智的make_tuple写法,这也是标准的工作方式:

对于类型...

中的每个Ti,VTypes中的相应类型Vi...是 std::d ecay::type 除非应用 std::d ecay 导致 std::reference_wrapper 对于某些类型 X,在这种情况下,推导出 类型为 X&。

评论太长了,但这是 -fsanitize=undefined 和 -fsanitize-address 的输出:

$ /llvm/8.0.0/bin/clang++ deleteme.cpp -fsanitize=undefined -std=c++17
$ ./a.out 
42
/llvm/8.0.0/bin/../include/c++/v1/string:2317:12: runtime error: reference binding to null pointer of type 'std::__1::basic_string<char>'
$ /llvm/8.0.0/bin/clang++ deleteme.cpp -fsanitize=address -std=c++17
$ ./a.out 
=================================================================
==32167==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffee3c52820 at pc 0x00010bfae2f2 bp 0x7ffee3c527b0 sp 0x7ffee3c527a8
READ of size 4 at 0x7ffee3c52820 thread T0
#0 0x10bfae2f1 in main (a.out:x86_64+0x1000012f1)
#1 0x7fff56c39014 in start (libdyld.dylib:x86_64+0x1014)
Address 0x7ffee3c52820 is located in stack of thread T0 at offset 96 in frame
#0 0x10bfade3f in main (a.out:x86_64+0x100000e3f)
This frame has 12 object(s):
[32, 56) 'spt'
[96, 104) 'ref.tmp' <== Memory access at offset 96 is inside this variable
[128, 132) 'ref.tmp1'
[144, 148) 'ref.tmp2'
[160, 184) 'ref.tmp3'
[224, 248) 'ref.tmp7'
[288, 296) 'st2'
[320, 324) 'ref.tmp12'
[336, 340) 'ref.tmp13'
[352, 376) 'spt2'
[416, 440) 'ref.tmp16'
[480, 504) 'ref.tmp19'
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-scope (a.out:x86_64+0x1000012f1) in main
Shadow bytes around the buggy address:
0x1fffdc78a4b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc78a4c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc78a4d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc78a4e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc78a4f0: 00 00 00 00 00 00 00 00 f1 f1 f1 f1 00 00 00 f2
=>0x1fffdc78a500: f2 f2 f2 f2[f8]f2 f2 f2 f8 f2 f8 f2 f8 f8 f8 f2
0x1fffdc78a510: f2 f2 f2 f2 f8 f8 f8 f2 f2 f2 f2 f2 f8 f2 f2 f2
0x1fffdc78a520: f8 f2 f8 f2 f8 f8 f8 f2 f2 f2 f2 f2 f8 f8 f8 f2
0x1fffdc78a530: f2 f2 f2 f2 f8 f8 f8 f3 f3 f3 f3 f3 00 00 00 00
0x1fffdc78a540: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x1fffdc78a550: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable:           00
Partially addressable: 01 02 03 04 05 06 07 
Heap left redzone:       fa
Freed heap region:       fd
Stack left redzone:      f1
Stack mid redzone:       f2
Stack right redzone:     f3
Stack after return:      f5
Stack use after scope:   f8
Global redzone:          f9
Global init order:       f6
Poisoned by user:        f7
Container overflow:      fc
Array cookie:            ac
Intra object redzone:    bb
ASan internal:           fe
Left alloca redzone:     ca
Right alloca redzone:    cb
Shadow gap:              cc
==32167==ABORTING
Abort trap: 6