为什么std::any_cast不支持隐式转换?

42

std::any_cast为什么会在实际存储类型可以隐式转换为请求类型时抛出std::bad_any_cast异常?

例如:

std::any a = 10;  // holds an int now
auto b = std::any_cast<long>(a);   // throws bad_any_cast exception
为什么不允许这样做?有没有方法可以绕过去实现隐式转换(如果我们不知道 std::any 正确的类型)?

为什么不允许这样做?有没有方法可以绕过去实现隐式转换(如果我们不知道 std::any 正确的类型)?


2
这既不可能也不必要!事实上,其他语言对于拆箱也有完全相同的限制。我想不出任何一个场景会出现问题。 - Konrad Rudolph
5个回答

52

std::any_cast 是根据 typeid 指定的。引用cppreference 上的一段话:

如果所请求的 ValueTypetypeid 与操作数的内容不匹配,则抛出 std::bad_any_cast

由于typeid不允许实现“推断”隐式转换是否可能,因此我认为any_cast也无法知道它是否可能。

换句话说,std::any 提供的类型擦除仅依赖于运行时可用的信息。而且这些信息并不像编译器为了确定转换所具有的信息那样丰富。这就是 C++17 中类型擦除的代价。


6
@Timo - 不可能吗?如果你有想法的话,那将是一次重大突破。我不知道有什么变通方法。要尝试转换,你需要获得正确类型的引用以擦除对象。但要获得它,你需要在静态情况下先知道类型。这是一个循环依赖的问题,考虑得越多就越复杂。目前,C++并不支持任何此类功能。 - StoryTeller - Unslander Monica
@MFH:我不知道是什么引起了您的这种观察... 如果您无法想象如何在此处使用 dynamic_cast,那么我建议您了解一下类型抹消技术。例如,shared_ptr<void> 如何知道如何删除底层值?如果您能回答这个问题,那么您应该能够弄清楚如何在 any 实现中使用 dynamic_cast - Matthieu M.
@Simple:抱歉,我在上一条评论中提到了一个可能会让你困惑的快捷方式;在这条第一条评论中,我提到了facilities。也就是说,dynamic_cast实现的机制具有在不使用模板代码的情况下从源指针到目标指针的能力。请参见https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/libsupc%2B%2B/dyncast.cc以获取示例实现。 - Matthieu M.
@MatthieuM。你有没有实际检查过你链接的代码?因为当你的void *不引用多态类型时,它注定会失败!所以,除非你想强制每个std::any用户增加额外的开销(比如std::function),否则不可能进行这种转换! - MFH
2
@MatthieuM,你不应该有两种提取数据的方法,因为any_cast是设计用来模仿C++-cast的。无论如何,“你是对的” - 我忽视了深拷贝的要求,这使得vtables已经成为std::any必需的部分,所以添加更广泛的转换支持是可能的(尽管此功能会放大悬挂引用的问题)。 - MFH
显示剩余6条评论

21

要做到你想要的,需要完整的代码反射和实例化。这意味着每个类型的每个细节都必须保存在每个二进制文件中(以及每个类型的每个函数的每个签名!还有任何地方的每个模板!),当您要将any转换为X类型时,您将传递与X有关的数据到any中,它包含关于其所包含类型的足够信息,可以基本上尝试编译转换为X并失败或成功。

有一些语言可以做到这一点;每个二进制文件都带有IR bytecode(或原始源)和解释器/编译器。这些语言在大多数任务上比C++慢2倍或更多,并且具有显着更大的内存占用。可能可以在不付出成本的情况下拥有这些功能,但据我所知,没有这种语言。

C ++没有这种能力。相反,在编译期间几乎忘记了有关类型的所有事实。对于任何类型,它都会记住一个typeid,可以用于获得精确匹配,并了解如何将其存储转换为该精确匹配。


通常使用JIT编译来实现与C ++相当的速度。 C ++(以及rust等)的好处在于它们是可预测和确定性的(对于机器而言,而不是开发人员 - 如果您没有未定义的行为),严格 - 不执行GC意味着您对分配具有严格的控制权。 - Benjamin Gruenbaum
@BenjaminGruenbaum 我同意:2倍因素是“可比较的”。也许可以编写一种语言,它可以完成所有上述功能,并且没有近2倍的减速,但我还没有遇到过。我知道JIT可以处理一些微基准测试,并在某些情况下与C++的速度相匹配。可能只是JIT语言中近乎统一使用GC才是真正的减速,而一个没有GC的JIT语言可以处理全代码库反射/具体化而不会受到性能影响。请告诉我这个语言,我很想看看。 - Yakk - Adam Nevraumont

6

std::any必须使用类型抹除来实现。这是因为它可以存储任何类型,而且不能是一个模板。目前在C++中没有其他功能可以实现这一点。

这意味着std::any将存储一个类型抹除指针void*,而std::any_cast将把该指针转换为指定的类型,然后就完成了。它只是在转换之前使用typeid进行一致性检查,以检查您将其转换为的类型是否与存储在任何中的类型相同。

使用当前实现将无法允许隐式转换。暂时忽略typeid检查,想想看。

std::any_cast<long>(a);
a 存储的是 int 而不是 long。那么 std::any 怎么知道呢?它只能将其 void* 强制转换为指定的类型,然后解引用并返回它。从一个类型到另一个类型的指针强制转换属于严格别名违规,会导致未定义行为,所以这是一个不好的想法。 std::any 必须存储其中存储的实际对象类型,但这是不可能的。你现在无法在 C++ 中存储类型。 它可以维护一系列类型及其对应的 typeid,并切换以获取当前类型并执行隐式转换。 但是,无法为您要使用的每个单个类型执行此操作。用户定义的类型无法正常工作,并且您必须依赖诸如宏之类的东西来“注册”您的类型并为其生成适当的开关案例1
也许可以尝试以下方式:
template<typename T>
T any_cast(const any &Any) {
  const auto Typeid = Any.typeid();
  if (Typeid == typeid(int))
    return *static_cast<int *>(Any.ptr());
  else if (Typeid == typeid(long))
    return *static_cast<long *>(Any.ptr());
  // and so on. Add your macro magic here.

  // What should happen if a type is not registered?
}

这是一个好的解决方案吗?远远不是。交换机成本高昂,而C++的口号是“你不会为你不使用的东西付费”,所以目前没有办法实现这一点。这种方法也很“hacky”并且非常脆弱(如果您忘记注册类型会发生什么)。简而言之,像这样做的可能好处不值得麻烦。
有没有解决方法可以允许隐式转换(在 std::any 持有的确切类型未知的情况下)?
是的,使用上述宏注册方法自己实现 std::any(或类似类型)和 std::any_cast。我不建议这样做。如果您不知道并且无法知道 std::any 存储什么类型,并且需要访问它,则可能存在设计缺陷。
1: 实际上不知道是否可能,在宏滥用方面我不太擅长。您还可以在您的自定义实现中硬编码您的类型,或者使用单独的工具。

2
这种通过注册进行转换的技巧确实可行。我做过。但是生成的代码绝对不是您期望在标准语言功能中找到的那种代码。它看起来更像是特殊领域的函数(这正是我实现它时所做的)。 - Cort Ammon

1

如果请求的类型 id 与存储的类型 id 不同,可以尝试使用备用隐式转换来实现此操作。但这将涉及成本,并违反"不为不使用的功能付费"原则。另一个any的缺点是无法存储数组。

std::any("blabla");

这样做是可行的,但它会存储一个char const*,而不是一个数组。你可以在自己定制的any中添加这样的功能,但是你需要通过以下方式存储指向字符串字面量的指针:

any(&*"blabla");

这有点奇怪。标准委员会的决定是一种妥协,永远不会满足所有人,但幸运的是你可以实现自己的any

你也可以扩展any来存储并调用类型擦除的函数对象,但这也不受标准支持。


1
这个问题表述不清;原则上,可以进行到正确类型的隐式转换,但被禁用了。 这个限制可能存在是为了保持一定的安全性或模拟在C版的any (void*)中必须进行(通过指针)的显式转换。 (下面的示例实现表明这是可能的。)
话虽如此,你的目标代码仍然不能工作,因为在转换之前需要知道确切的类型,但原则上这是可行的。
any a = 10;  // holds an int now
long b = int(a); // possible but today's it needs to be: long b = any_cast<int>(a);

为了展示隐式转换在技术上是可能的(但在运行时可能会失败):
#include<boost/any.hpp>

struct myany : boost::any{
    using boost::any::any;
    template<class T> operator T() const{return boost::any_cast<T>(*this);}
};

int main(){

    boost::any ba = 10;
//  int bai = ba; // error, no implicit conversion

    myany ma = 10; // literal 10 is an int
    int mai = ma; // implicit conversion is possible, other target types will fail (with an exception)
    assert(mai == 10);

    ma = std::string{"hello"};
    std::string mas = ma;
    assert( mas == "hello" );
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接