【问题标题】:why doesn't std::any_cast support implicit conversion?为什么 std::any_cast 不支持隐式转换?
【发布时间】:2018-03-22 11:53:16
【问题描述】:

当可以从实际存储的类型隐式转换到请求的类型时,为什么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 持有的确切类型未知)?

【问题讨论】:

标签: c++ c++17


【解决方案1】:

std::any_casttypeid 的形式指定。在此引用cppreference

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

由于typeid 不允许实现“找出”隐式转换是可能的,因此(据我所知)any_cast 也无法知道这是可能的。

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

【讨论】:

  • 好吧,如果它与 typeid 相关联并且我理解您的编辑,那么在不知道确切类型的情况下无法解决问题?
  • @Timo - 不可能吗?如果你有一个想法,那将是一个重大突破。我不知道解决方法。甚至要尝试转换,您需要获得对已擦除对象的正确类型的引用。但要获得它,您需要事先知道类型(静态)。这是一个先有鸡还是先有蛋的问题,你想得越多。 C++ 目前不支持任何类型的东西。
  • 当然可以创建一个具有更广泛合同的std::any。例如,任何派生到基础或交叉转换都可以基于dynamic_cast 设施进行处理,并且任何原始转换都可以进行硬编码。然而,这是可取的完全是另一回事,最值得注意的是因为它不会那么便宜。就像检查typeid
  • @MFH:我不知道是什么导致了这种观察......如果你无法想象如何在这里使用dynamic_cast,我邀请你了解一下类型擦除技术。例如,shared_ptr&lt;void&gt; 是如何知道如何删除底层价值的?如果你能回答这个问题,那么你应该能够弄清楚如何在any 实现中使用dynamic_cast
  • @MatthieuM。您不应该有两种方法来提取数据,因为 any_cast 旨在模仿 C++ 转换。无论如何你是对的 - 我忽略了对深拷贝的要求,这使得 vtables 对于 std::any 来说已经是必需的,所以是的,添加更广泛的转换支持是可能的(尽管悬空引用的问题是被此功能放大)
【解决方案2】:

要做你想做的事,你需要完整的代码反射和具体化。这意味着每种类型的每个细节都必须保存到每个二进制文件中(以及每种类型的每个函数的每个签名!以及任何地方的每个模板!),并且当您要求将任何类型转换为 X 类型时,您会通过将有关 X 的数据放入 any 中,其中将包含有关其包含的类型的足够信息,以基本上尝试编译到 X 的转换并且失败与否。

有些语言可以做到这一点;每个二进制文件都附带 IR 字节码(或原始源代码)和解释器/编译器。在大多数任务中,这些语言往往比 C++ 慢 2 倍或更多,并且内存占用明显更大。或许可以免费获得这些功能,但没有人拥有我所知道的那种语言。

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

【讨论】:

  • 执行此操作的语言通常使用 JIT 编译来实现与 C++ 相当的速度。 C++(和 rust 等)的好处是它们是可预测的和确定性的(对于机器,而不是开发人员 - 如果你没有未定义的行为)和严格 - 不做 GC 意味着你对分配有严格的控制权例子。
  • @BenjaminGruenbaum 我同意:因子 2 是“可比的”。有可能编写一种能够完成上述所有操作并且速度不会降低约 2 倍的语言,但我还没有遇到它。而且我知道 JIT 可以处理一些微基准测试并在某些情况下匹配 C++ 速度。可能只是在 JIT 语言中几乎统一使用 GC 才是真正的减速,而没有 GC 的 JIT 语言可以处理完整的代码库反射/具体化而不会影响性能。请指出我的语言,我很想看看。
【解决方案3】:

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

这意味着std::any 将存储一个类型擦除的指针,void*std::any_cast 将该指针转换为指定的类型,仅此而已。它只是在使用 typeid 之前进行完整性检查,以检查您将其转换为的类型是否是存储在 any 中的类型。

使用当前实现是不可能允许隐式转换的。考虑一下(暂时忽略typeid 检查)。

std::any_cast<long>(a);

a 存储 int 而不是 longstd::any 应该怎么知道?它可以将其void* 转换为指定的类型,取消引用并返回它。将指针从一种类型转换为另一种类型是严格的别名违规并导致 UB,所以这是个坏主意。

std::any 必须存储存储在其中的对象的实际类型,这是不可能的。您现在不能在 C++ 中存储类型。它可以维护一个类型列表以及它们各自的typeids 并切换它们以获取当前类型并执行隐式转换。但是对于您要使用的每个 单一类型,没有办法做到这一点。用户定义的类型无论如何都不起作用,您必须依靠诸如宏之类的东西来“注册”您的类型并为其生成适当的 switch case1

可能是这样的:

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 持有的确切类型未知)?

是的,使用上面提到的宏寄存器方法1自己实现std::any(或类似类型)和std::any_cast。我不会推荐它。如果您不知道也无法知道std::any 存储的类型并需要访问它,则可能存在设计缺陷。


1:实际上不知道这是否可能,我在宏(ab)使用方面不是那么好。您还可以为自定义实现硬编码类型或使用单独的工具。

【讨论】:

  • 这种带有注册的铸造技巧当然是可能的。我已经做到了。但生成的代码绝对不是您希望在标准语言功能中找到的那种代码。它看起来更像是一个特殊的领域特定功能(这正是我实现它时的样子)
【解决方案4】:

如果请求类型的类型 id 与存储类型的类型 id 不同,这可以通过尝试应急隐式转换来实现。但这会涉及成本,因此违反了"not pay for what you don't use" 原则。例如,另一个any 的缺点是无法存储数组。

std::any("blabla");

会起作用,但它会存储char const*,而不是数组。您可以在自己定制的any 中添加这样的功能,但是您需要通过执行以下操作来存储指向字符串文字的指针:

any(&*"blabla");

这有点奇怪。标准委员会的决定是一种妥协,永远不会让每个人都满意,但幸运的是,您可以选择实施自己的any

例如,您还可以扩展 any 以存储然后调用类型擦除的 functors,但标准也不支持这一点。

【讨论】:

    【解决方案5】:

    这个问题不好提出; 原则上可以隐式转换为正确的类型,但已禁用。 存在此限制可能是为了保持一定程度的安全性或模仿 any (void*) 的 C 版本中必要的显式转换(通过指针)。 (下面的示例实现表明这是可能的。)

    话虽如此,您的目标代码仍然无法工作,因为您需要在转换之前知道确切的类型,但这原则上可以工作:

    any a = 10;  // holds an int now
    long b = int(a); // possible but today's it should 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" );
     }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-06
      • 1970-01-01
      • 2021-06-01
      • 2010-10-04
      • 2014-03-02
      • 2021-05-24
      • 1970-01-01
      相关资源
      最近更新 更多