1

我有一个std::filesystem::directory_entry从 directory_iterator 打印的项目。另一方面,我有一个完全独立的类重载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到,但我不确定。Varcout

您能否解释一下实际发生了什么以及为什么代码是错误的?

我四处测试。对于这个问题,无论我投入什么似乎都可以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<<可以访问类的私有)。但是,如果我只是分开而不交朋友,它就失败了。

主.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 并获得了类似的结果)。

4

1 回答 1

0

有了曼苏尔的提示,我想我发现了。

这段代码很危险。

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

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

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

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

在此之前 directory_entry可以path 像这样使用隐式 c-tor转换为

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

这就是为什么 ostream << directory_entry如果没有ostream<<明确定义就可以工作的原因。

朋友

朋友实际上范围转换器可见性。这也有效

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

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

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 的绝妙想法: How do I check if an std::variant can hold a certain type

于 2020-08-30T22:48:36.430 回答