转换持有多态类型的模板时的未定义行为
undefined behavior when casting template holding a polymorphic type
出于类型擦除的原因,我有一个模板 A<T>
可以容纳任何数据类型。当 A
持有派生自 Base
的多态类型 Derived
并且我将其转换为 A<Base>
时,GCC 的未定义行为消毒程序报告 运行 时间错误:
#include <iostream>
struct I
{
virtual ~I() = default;
};
template<typename T>
struct A : public I
{
explicit A(T&& value) : value(std::move(value)) {}
T& get() { return value; }
private:
T value;
};
struct Base
{
virtual ~Base() = default;
virtual void fun()
{
std::cout << "Derived" << std::endl;
}
};
struct Derived : Base
{
void fun() override
{
std::cout << "Derived" << std::endl;
}
};
int main()
{
I* a_holding_derived = new A<Derived>(Derived());
A<Base>* a_base = static_cast<A<Base>*>(a_holding_derived);
Base& b = a_base->get();
b.fun();
return 0;
}
编译&运行
$ g++ -fsanitize=undefined -g -std=c++11 -O0 -fno-omit-frame-pointer && ./a.out
输出:
main.cpp:37:62: runtime error: downcast of address 0x000001902c20 which does not point to an object of type 'A'
0x000001902c20: note: object is of type 'A<Derived>'
00 00 00 00 20 1e 40 00 00 00 00 00 40 1e 40 00 00 00 00 00 00 00 00 00 00 00 00 00 21 00 00 00
^~~~~~~~~~~~~~~~~~~~~~~
vptr for 'A<Derived>'
#0 0x400e96 in main /tmp/1450529422.93451/main.cpp:37
#1 0x7f35cb1a176c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2176c)
#2 0x400be8 (/tmp/1450529422.93451/a.out+0x400be8)
main.cpp:38:27: runtime error: member call on address 0x000001902c20 which does not point to an object of type 'A'
0x000001902c20: note: object is of type 'A<Derived>'
00 00 00 00 20 1e 40 00 00 00 00 00 40 1e 40 00 00 00 00 00 00 00 00 00 00 00 00 00 21 00 00 00
^~~~~~~~~~~~~~~~~~~~~~~
vptr for 'A<Derived>'
#0 0x400f5b in main /tmp/1450529422.93451/main.cpp:38
#1 0x7f35cb1a176c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2176c)
#2 0x400be8 (/tmp/1450529422.93451/a.out+0x400be8)
Derived
我有两个问题:
- 消毒剂的输出是否正确?
- 如果是,从
A<Derived>
到 A<Base>
的有效转换会是什么样子?
问题是 A<Base>
和 A<Derived>
彼此没有任何关系 完全 。它们的表示可能完全不同。对于您尝试执行的演员表,A<Base>
是 A<Derived>
的基数 class 显然不是这种情况。
看来,您想创建类似于值类型的智能指针之类的东西。副手,我不确定是否可以创建支持所有必要转换的值类型。如果在需要支持转换的类型组中存在特定需求或已知的共同基础 class,则可以实施相应的 class。
我不确定您的设计目标,但为了正确看待讨论,这里有一个典型的类型擦除示例:一个 class Foo
公开了 class Foo
的擦除调用=14=]:
#include <memory>
#include <type_traits>
#include <utility>
class Foo
{
struct ImplBase
{
virtual ~ImplBase() = default;
virtual int bar(int, int) = 0; // This line is the whole point!
};
std::unique_ptr<ImplBase> impl;
template <typename T> struct Impl : ImplBase
{
Impl(T t) t_(std::move(t)) {}
int bar(int a, int b) override { return t_.bar(a, b); }
T t_;
};
public:
template <typename T>
Foo(T && x)
: impl(new Impl<typename std::decay<T>::type>(std::forward<T>(x)))
{}
int bar(int a, int b) // Not virtual! Foo is a "value-like" class.
{
return impl->bar(a, b);
}
};
这种方法的实用性在于,您现在可以拥有一个使用 Foo
的接口类型,并且可以使用 any 调用此接口在结构上满足 Impl
要求的类型(您当然会在不参考实施细节的情况下记录)。
例如,考虑以下函数:
void DoSomething(Foo a, int x, int y)
{
UpdateCounter(a.bar(x, y));
}
这个函数可以在一个单独的翻译单元中定义和编译,永远不会再被触及。但是未来的用户,可能永远不会与 DoSomething
作者有因果联系,可以传递任意对象,这些对象公开了 bar
函数:
struct X { double bar(long int, int); };
struct Y { char bar(int, float, bool = false); };
DoSomething(Foo(X{}), 10, 20);
DoSomething(Foo(Y{}), 20, 10);
备注:
- 类型擦除提供 临时多态性。
- 对客户端类型的要求是结构性的,与继承无关。想想 "duck typing" 或 "concept".
- 类型擦除设计公开了功能,而不是层次相关性。
- 如果您要求
Impl
可复制(这转化为对 T
的要求),您可以使 Foo
可复制。
- 我们使用原始的
new
;没有分配器支持。类型擦除分配器支持已被证明非常具有挑战性,特别是如果类型擦除状态应该是可复制的。
出于类型擦除的原因,我有一个模板 A<T>
可以容纳任何数据类型。当 A
持有派生自 Base
的多态类型 Derived
并且我将其转换为 A<Base>
时,GCC 的未定义行为消毒程序报告 运行 时间错误:
#include <iostream>
struct I
{
virtual ~I() = default;
};
template<typename T>
struct A : public I
{
explicit A(T&& value) : value(std::move(value)) {}
T& get() { return value; }
private:
T value;
};
struct Base
{
virtual ~Base() = default;
virtual void fun()
{
std::cout << "Derived" << std::endl;
}
};
struct Derived : Base
{
void fun() override
{
std::cout << "Derived" << std::endl;
}
};
int main()
{
I* a_holding_derived = new A<Derived>(Derived());
A<Base>* a_base = static_cast<A<Base>*>(a_holding_derived);
Base& b = a_base->get();
b.fun();
return 0;
}
编译&运行
$ g++ -fsanitize=undefined -g -std=c++11 -O0 -fno-omit-frame-pointer && ./a.out
输出:
main.cpp:37:62: runtime error: downcast of address 0x000001902c20 which does not point to an object of type 'A'
0x000001902c20: note: object is of type 'A<Derived>'
00 00 00 00 20 1e 40 00 00 00 00 00 40 1e 40 00 00 00 00 00 00 00 00 00 00 00 00 00 21 00 00 00
^~~~~~~~~~~~~~~~~~~~~~~
vptr for 'A<Derived>'
#0 0x400e96 in main /tmp/1450529422.93451/main.cpp:37
#1 0x7f35cb1a176c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2176c)
#2 0x400be8 (/tmp/1450529422.93451/a.out+0x400be8)
main.cpp:38:27: runtime error: member call on address 0x000001902c20 which does not point to an object of type 'A'
0x000001902c20: note: object is of type 'A<Derived>'
00 00 00 00 20 1e 40 00 00 00 00 00 40 1e 40 00 00 00 00 00 00 00 00 00 00 00 00 00 21 00 00 00
^~~~~~~~~~~~~~~~~~~~~~~
vptr for 'A<Derived>'
#0 0x400f5b in main /tmp/1450529422.93451/main.cpp:38
#1 0x7f35cb1a176c in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2176c)
#2 0x400be8 (/tmp/1450529422.93451/a.out+0x400be8)
Derived
我有两个问题:
- 消毒剂的输出是否正确?
- 如果是,从
A<Derived>
到A<Base>
的有效转换会是什么样子?
问题是 A<Base>
和 A<Derived>
彼此没有任何关系 完全 。它们的表示可能完全不同。对于您尝试执行的演员表,A<Base>
是 A<Derived>
的基数 class 显然不是这种情况。
看来,您想创建类似于值类型的智能指针之类的东西。副手,我不确定是否可以创建支持所有必要转换的值类型。如果在需要支持转换的类型组中存在特定需求或已知的共同基础 class,则可以实施相应的 class。
我不确定您的设计目标,但为了正确看待讨论,这里有一个典型的类型擦除示例:一个 class Foo
公开了 class Foo
的擦除调用=14=]:
#include <memory>
#include <type_traits>
#include <utility>
class Foo
{
struct ImplBase
{
virtual ~ImplBase() = default;
virtual int bar(int, int) = 0; // This line is the whole point!
};
std::unique_ptr<ImplBase> impl;
template <typename T> struct Impl : ImplBase
{
Impl(T t) t_(std::move(t)) {}
int bar(int a, int b) override { return t_.bar(a, b); }
T t_;
};
public:
template <typename T>
Foo(T && x)
: impl(new Impl<typename std::decay<T>::type>(std::forward<T>(x)))
{}
int bar(int a, int b) // Not virtual! Foo is a "value-like" class.
{
return impl->bar(a, b);
}
};
这种方法的实用性在于,您现在可以拥有一个使用 Foo
的接口类型,并且可以使用 any 调用此接口在结构上满足 Impl
要求的类型(您当然会在不参考实施细节的情况下记录)。
例如,考虑以下函数:
void DoSomething(Foo a, int x, int y)
{
UpdateCounter(a.bar(x, y));
}
这个函数可以在一个单独的翻译单元中定义和编译,永远不会再被触及。但是未来的用户,可能永远不会与 DoSomething
作者有因果联系,可以传递任意对象,这些对象公开了 bar
函数:
struct X { double bar(long int, int); };
struct Y { char bar(int, float, bool = false); };
DoSomething(Foo(X{}), 10, 20);
DoSomething(Foo(Y{}), 20, 10);
备注:
- 类型擦除提供 临时多态性。
- 对客户端类型的要求是结构性的,与继承无关。想想 "duck typing" 或 "concept".
- 类型擦除设计公开了功能,而不是层次相关性。
- 如果您要求
Impl
可复制(这转化为对T
的要求),您可以使Foo
可复制。 - 我们使用原始的
new
;没有分配器支持。类型擦除分配器支持已被证明非常具有挑战性,特别是如果类型擦除状态应该是可复制的。