【问题标题】:Xcode 10 call to unavailable function std::visitXcode 10 调用不可用的函数 std::visit
【发布时间】:2019-02-18 00:27:46
【问题描述】:

使用 Xcode 10 GM 编译以下程序时:

#include <iostream>
#include <string>
#include <variant>

void hello(int) {
    std::cout << "hello, int" << std::endl;
}

void hello(std::string const & msg) {
    std::cout << "hello, " << msg << std::endl;
}

int main(int argc, const char * argv[]) {
    // insert code here...
    std::variant< int, std::string > var;

    std::visit
    (
        []( auto parameter )
        {
            hello( parameter );
        },
        var
     );

    return 0;
}

我收到以下错误:

main.cpp:27:5:调用不可用函数“visit”:在 macOS 10.14 中引入

但是,如果我将最小部署目标更改为 macOS 10.14,即使我运行的是 macOS 10.13,代码也可以正常编译并且可以正常工作。

由于std::visit 是函数模板,并且不应该依赖于操作系统版本(我通过在低于实际支持的mac 版本上运行代码来证明这一点),这是否应该被视为错误并报告给Apple 或者这是预期的行为?

在为 iOS 编译时也会发生同样的情况(至少需要 iOS 12)。

【问题讨论】:

  • 现在这对我来说听起来像是预期的行为。您的操作系统无关紧要,部署目标是唯一重要的事情。对于 =10.14 它会。还是我错过了什么?
  • 但是如果我使用部署目标 iOS 12 进行编译,链接器还将包含其他仅适用于 iOS 12 的功能,并且二进制文件将不再适用于 iOS 8。我不明白为什么这个函数是C++17 standard 的一部分并且不应该依赖于操作系统版本,它依赖于macos 10.14 和ios 12?
  • std::visit 是一个函数模板,因此它不能成为仅随 iOS 12 和 macOS 10.14 一起提供的某些动态库的一部分。作为一个函数模板,它完全在标头中实现,并在调用代码中内联,因此不应该依赖于任何操作系统版本。
  • @DoDo:这是基于过度简化的错误假设。至少非常,允许你提出的建议绝对是一场后勤噩梦。实际上,支持它没有任何好处。最好有 一个 版本的您使用的实现,并且不关心哪些部分可以构建到可执行文件中,哪些部分可以在运行时库中找到。

标签: c++ ios xcode c++17


【解决方案1】:

所有可能引发 std::bad_variant_accessstd::variant 功能都在标准头文件中标记为从 macOS 10.14(以及相应的 iOS、tvOS 和 watchOS)开始可用。这是因为虚拟的std::bad_variant_access::what()方法不是inline,因此定义在libc++.dylib中(由操作系统提供)。

有几种解决方法(技术上都是未定义的行为),按我的个人喜好排序:

1) 参与实施

std::visit 仅在变量参数之一是 valueless_by_exception 时抛出。查看实现为您提供了使用以下解决方法的线索(假设 vs 是变体的参数包):

if (... && !vs.valueless_by_exception() ) {
  std::__variant_detail::__visitation::__variant::__visit_value(visitor, vs...);
} else {
  // error handling
}

缺点:可能会与未来的 libc++ 版本中断。丑陋的界面。

专业人士:编译器可能会在它崩溃时对你大喊大叫,并且可以轻松调整解决方法。您可以针对丑陋的界面编写一个包装器。

2) 抑制可用性编译器错误...

_LIBCPP_DISABLE_AVAILABILITY 添加到项目设置预处理器宏 (GCC_PREPROCESSOR_DEFINITIONS)

缺点:这也会抑制其他可用性保护(shared_mutexbad_optional_access 等)。

2a) ... 并使用它

事实证明它已经在 High Sierra 中工作了,而不仅仅是 Mojave(我已经测试到 10.13.0)。

在 10.12.6 及以下版本中,您会收到运行时错误:

dyld: Symbol not found: __ZTISt18bad_variant_access
  Referenced from: [...]/VariantAccess
  Expected in: /usr/lib/libc++.1.dylib
 in [...]/VariantAccess
Abort trap: 6

第一行解开为_typeinfo for std::bad_variant_access。这意味着动态链接器(dyld)找不到指向引言中提到的what()方法的vtable。

缺点:仅适用于某些操作系统版本,如果它不起作用,您只有在启动时才知道。

专业版: 保持原始界面。

2b) ...并提供您自己的异常实现

在您的项目源文件之一中添加以下行:

// Strongly undefined behaviour (violates one definition rule)
const char* std::bad_variant_access::what() const noexcept {
    return "bad_variant_access";
}

我已经针对 10.10.0、10.12.6、10.13.0、10.14.1 上的独立二进制文件对此进行了测试,即使导致 std::bad_variant_access 被抛出,我的示例代码也能正常工作,并被 std::exception const&amp; ex 捕获,并调用虚拟ex.what()

缺点:我的假设是,当使用 RTTI 或跨二进制边界(例如不同的共享对象库)进行异常处理时,这个技巧会失效。但这只是一个假设,这就是我将这个解决方法放在最后的原因:我不知道它什么时候会损坏以及会出现什么症状。

专业版: 保持原始界面。可能适用于所有操作系统版本。

【讨论】:

  • 有谁知道为什么std::bad_variant_access::what() 不是inline
  • 对选项 2 的补充):可以改为定义 _LIBCPP_NO_EXCEPTIONS。抛出的 STL 方法将可用,但将 assert() 而不是抛出异常。
【解决方案2】:

发生这种情况是因为std::visithere 描述的情况下引发bad_variant_access 异常,并且由于该异常的实现取决于较新版本的 libc++,因此您需要使用发布此新版本的 iOS 和 macOS 版本(macOS 10.14 和 iOS 12)。

谢天谢地,当c++ exceptionsoff 时有一个可用的实现路径,它不依赖于较新的 libc++,所以如果可能的话,您可以使用该选项。

附: 关于您将最小部署目标增加到 10.14 并且仍然能够在 10.13 上正常运行程序的情况,我猜您在触发这个新异常时会遇到问题(因为依赖于的异常方法较新版本的 libc++ 将无法解析)。

【讨论】:

    【解决方案3】:

    这是另一种选择(对某些人来说不合适)。如果您已经在使用 Boost,那么您可以在针对 iOS 时使用Boost.Variant2

    #if MACRO_TO_TEST_FOR_IOS_LT_11
    #include <boost/variant2/variant.hpp>
    namespace variant = boost::variant2;
    #else
    #include <variant>
    namespace variant = std;
    #endif
    

    然后你可以在你的代码中使用variant::visit

    我仍在努力测试 iOS 目标版本(如果我们完全针对 iOS)。这就是我在上面使用MACRO_TO_TEST_FOR_IOS_LT_11 作为占位符的原因。

    【讨论】:

      【解决方案4】:

      尽管模板通常来自标头,但这并不意味着运行时目标无关紧要。这些模板是更广泛库的一部分,它们编译成的代码仍必须与该库的其余部分兼容。 整个标准库是一个单一版本是有意义的,并且该版本是可以在目标机器上工作的版本是有意义的。你能想象否则会发生什么混乱吗?

      这里的其他一些人给出了一些低级的实际原因,说明在这种特殊情况下版本统一很重要。就我个人而言,我认为在这种情况下最好忘记诸如“模板进入标题”之类的实现细节;你不应该关心它,而且你冒着做出破坏抽象的假设而没有什么好处的风险。只需编写合同代码就可以了。

      【讨论】:

      • 是的,在理想的世界里我会同意你的看法。但是,如果我希望我的应用程序支持旧版本的操作系统,那么根据合同进行编码将意味着基本上无法使用现代 C++。并且仅支持最新、最现代的操作系统版本,对于商业应用来说是不可能的。
      • @DoDo 事实上,如果您的目标是较旧的操作系统,那么您必须使用较旧的 C++。但没关系。直到最近,对我来说,C++11 还是一种奢侈!就是这样。我的回答是关于现实世界而不是理想世界:P
      • 我强烈反对。我认为,如果您希望支持较旧的操作系统,则强制使用较旧版本的 C++ 是不行的。这根本不是 C++ 的本质。而且,正如这里其他人所提到的,如果您知道自己在做什么,则可以在旧系统上使用 C++17 功能。
      • @DoDo 这绝对是 C++ 的本质;这就是为什么它有不同的版本,以及为什么所有主要编译器都有版本切换。当然,在某些情况下您可以升级(我使用 devtoolset 在 CentOS 6 上获取 GCC 4.8 和 C++11)。但是,特别是在企业中,您正在与您正在部署的生态系统的其他部分进行交互,因此版本管理是我们工作的关键部分。坚持使用“刚刚发明的东西”会让你周围的人非常头疼。当然,这些都没有真正改变事情的事实。圣诞快乐!
      • 很遗憾,选择较早版本的 C++ 标准不会使您的程序与较早版本的操作系统兼容。例如,即使您使用 C++11 而不是 C++17,但在 ArchLinux 上使用 GCC 8.2 编译代码,由于 GLIBC 的要求,它也无法在 CentOS 6 上运行。我只是说只有标头的 STL 容器不应该依赖于特定的操作系统版本,而它们实际上没有——这里的错误只是因为 Apple 决定只发布最新版本的 libc++。如果您静态链接它,它将起作用。也祝你圣诞快乐!
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-10-04
      • 1970-01-01
      • 2018-02-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多