Вывод для directory_entry конфликтует с перегруженным оператором ostream‹‹ для класса с вариантом

У меня есть проект, в котором я печатаю 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 в 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<< имеет доступ к приватному классу). Однако не получится, если я просто расстанусь и не подружусь.

основной.cpp:

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

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

переменная.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

переменная.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 с аналогичным результатом).


person Askold Ilvento    schedule 28.08.2020    source источник
comment
Прекрасно компилируется с магистралью Clang, и ошибка, которую я вижу с Clang 7.x, не похожа на вашу. ошибка, он жалуется на вариант, не имеющий соответствующего конструктора для инициализации. <source>:11:18: error: no matching constructor for initialization of 'VarType' (aka 'variant<long, basic_string<char> >') Var(T value) : _value{value} { ^ ~~~~~~~   -  person Mansoor    schedule 29.08.2020
comment
Под подобным я подразумевал, что clang также пытается инициализировать Var с помощью directory_entry. Если это исправлено в багажнике, следует ли считать это известной ошибкой компилятора? Спасибо за ссылку на богбол.   -  person Askold Ilvento    schedule 29.08.2020
comment
Нет, это не так. Но если вы хотите избежать этой ошибки, вы должны пометить свой конструктор как явный.   -  person Mansoor    schedule 29.08.2020
comment
Другими словами, directory_entry не имеет явного ostream& operator<<, и поэтому диалог должен использоваться в любом случае. Компилятор выбирает мой, то есть болеет и SFINAE не помогает. Эксплицит решает эту проблему. Версия компилятора Trunk имеет оператор ostream&‹‹ для directory_entry, компилятор не ищет преобразования, не так ли? Это объясняет большинство вопросов. Почему друг делает обходной путь?   -  person Askold Ilvento    schedule 29.08.2020


Ответы (1)


Думаю, благодаря намекам Мансура я узнал.

С-тор

Этот код опасен.

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

Избегайте этого. Компилятор попытается заменить такой c-tor в любом неявном преобразовании, где Var виден и может подойти. Отметить c-tor как explicit — самый простой способ ограничить такую ​​дикую замену. SFINAE и enable_if могут быть другими способами ограничения замен.

И плохая замена — это именно то, что ломает сложность в моем случае, потому что в старых комплаенсах directory_entry не имеет прямого определения ostream<<. Компилятор ищет конвертер и находит подходящий Var. Который может быть создан, но не может быть выполнен. Последнее хорошо, потому что если бы это было возможно, то ошибку невозможно было бы отследить.

Существует патч https://gcc.gnu.org/legacy-ml/gcc-patches/2018-06/msg01084.html около 2018 года (или, может быть, это скоро будет исправлено на нем), который ввел явный 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;
  }
};    

Но если std::ostream& объявлен как друг, а определение (или другое объявление) видно для ostream << directory_entry, когда оно снова нарушает усложнение, потому что другое объявление видно для дикой замены. Это объясняет, почему разделение на несколько блоков и использование друга создали обходной путь.

СФИНАЭ

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, можно найти здесь: hold-a-certain-type/45892305">Как проверить, может ли std::variant содержать определенный тип .

person Askold Ilvento    schedule 30.08.2020