directory_entry 的输出与具有变体的 class 的重载 ostream operator<< 冲突

Output for directory_entry conflicts with overloaded ostream operator<< for class with variant

我有一个项目,我从 directory_iterator 打印 std::filesystem::directory_entry。另一方面,我有一个完全独立的 class 重载 std::ostream& operator<<,它有一个模板构造函数,它初始化一个 std::variant 成员。

#include <variant>
#include <iostream>
#include <filesystem>

typedef std::variant<long, std::string> VarType;

class Var {
  VarType _value;
public:
  template<typename T>
  Var(T value) : _value{value} {
  }
};    

std::ostream& operator<< (std::ostream& stream, const Var&) {
  return stream;
}

int main() {
  std::cout << std::filesystem::directory_entry() << "\n";//tigger compling error
  return 0;
}

编译失败:

main.cpp: In instantiation of ‘Var::Var(T) [with T =
std::filesystem::__cxx11::directory_entry]’: main.cpp:25:49:  
required from here main.cpp:11:30: error: no matching function for
call to ‘std::variant<long int, double,
std::__cxx11::basic_string<char, std::char_traits<char>,
std::allocator<char> > >::variant(<brace-enclosed initializer list>)’ 
Var(T value) : _value{value} {
... several pages of output ... 

它似乎试图将 directory_entry 包装成 Var,然后再发送到 cout,但我不确定。

能否请您解释一下实际发生的情况以及代码错误的原因?

我测试了一下。对于这个问题,无论我在 variant 中输入什么,它似乎都会起作用,即使是单个变体也是错误的。这个

#include <variant>
#include <iostream>
#include <filesystem>

typedef std::variant<long, std::string> VarType;

class Var {
  VarType _value;
public:
  template<typename T>
  Var(T value) : _value{value} {
  }
};    

std::ostream& operator<< (std::ostream& stream, const VarType&) {
  return stream;
}

int main() {
  std::cout << std::filesystem::directory_entry() << "\n";
  return 0;
}

工作正常。如果我将 _value 初始化移动到 c-tor 主体编译失败并出现相同的逻辑错误,但对于 operator=,至少它是一致的。显然它适用于非模板化的 c-tor。

如果我将 ostream& operator<< 的实现移动到一个单独的单元中并将其定义为 Var 的友元,则编译通过(这是一种合适的解决方法,但不应该 operator<< 可以访问 class 的私有权限)。但是,如果我只是分开,不交朋友,那就失败了。

main.cpp:

#include "var.hpp"
#include <iostream>
#include <filesystem>

int main() {
  std::cout << std::filesystem::directory_entry() << "\n";
  std::cout << Var(1l) << "\n";
  return 0;
}

var.hpp:

#include <variant>
#include <ostream>

typedef std::variant<long, std::string> VarType;

class Var {
  VarType _value;
public:
  template<typename T>
  Var(T value) : _value{value} {
  }   
  friend std::ostream& operator<< (std::ostream& stream, const Var&);    //works
};    
//std::ostream& operator<< (std::ostream& stream, const Var&);    //instead above does not works

var.cpp:

#include "var.hpp"

std::ostream& operator<< (std::ostream& stream, const Var&) {
  return stream;
}

这让我完全迷路了。假设它尝试在 << 上调用 Var c-tor,这里应该没有区别。 为什么这样的改变很重要?

我用 g++8.4 构建(g++ -std=c++17 main.cpp var.cpp -lstdc++fs,我也试过 clang7.0 也有类似的结果)。

在 Mansoor 的提示下,我想我找到了。

C-tor

这段代码很危险。

class Var {
public:
  template<typename T>
  Var(T value)  {
  }  
};

避免这种情况。 编译器将尝试在 Var 可见且可能合适的任何隐式转换中替换此类 c-tor。 将 c-tor 标记为 explicit 是限制这种随意替换的最直接的方法。 SFINAE 和 enable_if 可以通过其他方式来限制替换。

在我的案例中,错误的替换正是打破复杂性的原因,因为在旧的 complies directory_entry 中没有 ostream<< 的直接定义。编译器寻找转换器并找到合适的Var。可以实例化但不能遵守。后者很好,因为如果可以的话,错误是无法追踪的。

有一个补丁 https://gcc.gnu.org/legacy-ml/gcc-patches/2018-06/msg01084.html 大约在 2018 年(或者可能很快就修复了)引入了显式 ostream << directory_entry

在此之前 directory_entry 可以通过隐式 c-tor 转换为 path

template<typename _Source,
     typename _Require = _Path<_Source>>
  path(_Source const& __source, format = auto_format)

这就是为什么 ostream << directory_entry 在没有明确定义 ostream<< 的情况下起作用的原因。

朋友

Friend 实际上限定了转换器的可见性。这也有效

class Var {
  VarType _value;
public:
  template<typename T>
  Var(T value) : _value{value} {
  }
  friend std::ostream& operator<< (std::ostream& stream, const Var&) {
    return stream;
  }
};    

但是,如果 std::ostream& 被声明为友元并且定义(或其他声明)对 ostream << directory_entry 可见,当它再次打破复杂化时,因为其他声明对于野性替换是可见的。这就解释了为什么分成几个单元并使用 friend 创建了一个解决方法。

SFINAE

SFINAE 不检查函数体。它仅适用于声明。 _value{value} 是正文。调用 SFINAE c-tor 应该像

  template< class T, typename = std::enable_if_t<std::disjunction_v<std::is_same<T, long>,  std::is_same<T, std::string>>>>
  Var(T value) : _value{value} {
  }

如何处理 std::variant 依赖 c-tor 的绝妙想法可以在这里找到: .