gcc libstdc++ u8path的弃用消息建议将严格的别名冲突作为解决方案?

gopyfrb3  于 2023-10-19  发布在  其他
关注(0)|答案(2)|浏览(120)

C++20弃用std::filesystem::u8path
在gcc.godbolt.org上运行

#include <filesystem>

std::string foo();

int main()
{
    auto path = std::filesystem::u8path(foo());
}

libstdc++ 13有一个弃用警告:

<source>:7:40: warning: 'std::filesystem::__cxx11::path std::filesystem::__cxx11::u8path(
const _Source&) [with _Source = std::__cxx11::basic_string<char>; _Require = path; _CharT
 = char]' is deprecated: use 'path((const char8_t*)&*source)' instead [-Wdeprecated-decla
rations]
    7 |     auto path = std::filesystem::u8path(foo());
      |                 ~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~

对我来说,建议的强制转换path((const char8_t*)&*source)看起来像是完全严格的别名冲突,因此是UB。
是这样吗?海湾合作委员会是否作出任何额外的保证,使这一法律的?
最后,如果我的路径存储在std::string中,并且我不想将所有内容重写为std::u8string,是否有更好的解决方法?

noj0wjuj

noj0wjuj1#

简而言之,在你的例子中有未定义的行为。然而,实际的原因不是严格的别名冲突,而是由于假设的严格别名冲突而导致的先决条件冲突。

由于严格的别名,没有未定义的行为

不存在严格的别名冲突([basic.lval] p11),因为对字符的任何访问都将发生在std::filesystem::path的构造函数或文件系统库的其他部分中,并且这些可以被允许以用户不能的方式进行类型双关。
(const char8_t*)&*本质上是数据的reinterpret_cast<const char8_t*>reinterpret_cast本身是有效的,即使通过指针访问对象是无效的。使用结果指针,您将调用以下构造函数:

template<class Source>
path(const Source& source, format fmt = auto_format);
  • 效果 *:设s为源的effective range或范围[first, last),如果需要,可使用encoding converted。查找s的检测格式,并构造一个类路径的对象,其格式中的路径名为s
  • [fs.class.path] std::path构造函数3
    path的格式检测、参数格式转换以及类型和编码转换都是通过数学或散文定义的。例如,编码转换在[fs.path.type.cvt] p3中定义:
    对于采用表示路径的字符序列的成员函数参数和返回字符串的成员函数,如果参数或返回值的值类型不同于path​::​value_type,则执行值类型和编码转换。对于参数或返回值,转换方法和要转换的编码由其值类型确定:
  • [...]
  • char8_t:编码为UTF-8。转换方式不详。

在实现这一点时,实现有很大的自由。例如,std::filesystem::path构造函数可以放宽别名规则。

违反前置条件导致的未定义行为

问题在于值类型的使用:
一个输入迭代器i支持表达式*i,导致一个对象类型T的值,称为迭代器的 * 值类型 *。
你的迭代器的类型是const char8_t*,间接(*i)对它无效,因为它假设违反了严格别名。因此,您传递给path构造函数的内容没有值类型,并且由于违反前提条件,因此行为未定义。

GCC严格放宽字符类型之间的别名

我无法在GCC文档中找到有关此的详细信息,但char8_t似乎能够别名char

auto alias(char c) {
    return *reinterpret_cast<char8_t*>(&c); // OK, no -Wstrict-aliasing
}

因此,您可能依赖于编译器扩展。

ohtdti5x

ohtdti5x2#

免责声明:我是P0482 (char8_t: A type for UTF-8 characters and strings)提案的作者,该提案在C20中弃用std::filesystem::u8path()
libstdc
警告提供的建议是正确的。通常这种强制转换是有问题的,但是charunsigned charstd::byte被授予了特殊的权力来别名其他类型。这是由[basic.lval]p11提供的,它指出:
如果一个程序试图通过一个glvalue来访问一个对象的存储值,而这个glvalue的类型不类似于以下类型之一,那么这个行为是未定义的:

  • 对象的动态类型,
  • 一个类型,它是与对象的动态类型相对应的有符号或无符号类型,或者
    *charunsigned charstd​::​byte类型。

...
但是,应尽可能避免别名转换,我推荐一种不同的解决方案。std::filesystem::path有一个构造函数模板,它接受一个经典的范围(一个迭代器对),并根据范围的值类型推导编码。对于保存在基于char的存储中的UTF-8输入,只需要一个范围适配器将基于char的值转换为char8_t。这可以在没有运行时开销的情况下执行:

std::string utf8_encoded_filename = ...;
auto char8_view =
    std::ranges::views::transform(utf8_encoded_filename,
                                  [](char c) { return (char8_t)c; });
std::filesystem::path p(char8_view.begin(), char8_view.end());

这样的视图适配器非常有用,因此charN_t视图适配器家族正在考虑在P2728 (Unicode in the Library, Part 1: UTF Transcoding)(撰写本文时为修订版6)中进行标准化。
如果需要,可以使用这样的视图适配器来提供u8path()替换,同样,不会造成运行时开销。

template<std::ranges::viewable_range R>
requires std::same_as<std::ranges::range_value_t<R>, char>
constexpr auto as_char8_t(R &&r) {
  return std::ranges::views::transform(std::forward<R>(r),
                                       [](char c) { return (char8_t)c; });
}
std::filesystem::path u8path(const std::string &s) {
  auto char8_view = as_char8_t(s);
  return std::filesystem::path(char8_view.begin(), char8_view.end());
}

如果你已经读到这一点,你现在可能想知道为什么u8path被弃用,如果它有可论证的用例。不幸的是,最初的提议缺乏对反对的辩护。其动机是,在弃用之前,标准库中只有两个接口需要保存UTF-8数据的基于char的输入;所有其他接口都期望基于char的输入保存以用于字符和字符串文字的普通文字编码或执行编码编码的文本数据。这两个接口是:

普通的文字编码和执行编码在实践中对于一些流行的(和一些不那么流行的)实现不是UTF-8,并且在可预见的未来预计仍然是这样。在没有类型系统支持的情况下,正确使用和维护以多种编码存储的文本是具有挑战性的,错误会导致质量和安全问题。重要的是,该标准不鼓励进一步使用char与UTF-8编码的文本;至少,不是可移植的代码,而且人们担心将来可能不得不为许多潜在的接口添加u8前缀版本。无法使用类型系统根据编码进行重载,这将成为泛型代码库开发的一个障碍。
注意:为了向后兼容现有的代码,将u8""字符串传递给std::filesystem::u8path(),虽然不推荐,但std::filesystem::u8path()通过P1423 (char8_t backward compatibility remediation)进行了修改,以接受char8_t的范围。

相关问题