1

I have a project where I print std::filesystem::directory_entry from directory_iterator. On the other side, I have a completely independent class with overload std::ostream& operator<<, that has a templated constructor, which initializes a std::variant member.

#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;
}

Compilation fails:

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 ... 

It seems it tries to wrap directory_entry into Var, before sending it to cout, but I am not sure.

Could you please explain what is actually going on and why code is wrong?

I tested around. For the issue, it seems to do no matter what I put into variant, even a single variant is ill. This one

#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;
}

works fine. If I move _value initialization into c-tor body compilation fails with the same logical error but for operator=, at least it is consistent. Evidently it works with non-templated c-tor.

If I move the implementation of ostream& operator<< into a separate unit and defines it as a friend of Var, compilation passes (it is a kind of suitable workaround, yet it was not supposed operator<< has access to private of class). However, it fails if I simply separate and do not make friends.

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;
}

That makes me completely lost. Assuming that it tries to invoke Var c-tor on << here should be no difference. Why such change does matter?

I build with g++8.4 (g++ -std=c++17 main.cpp var.cpp -lstdc++fs, also I tried clang7.0 with a similar result).

Askold Ilvento
  • 1,405
  • 1
  • 17
  • 20
  • 1
    [Compiles fine with Clang trunk](https://godbolt.org/z/rq169o), and the error I see with Clang 7.x is not similar to your posted error, it complains about the variant having no matching constructor for initialization. `:11:18: error: no matching constructor for initialization of 'VarType' (aka 'variant >') Var(T value) : _value{value} { ^ ~~~~~~~` – Mansoor Aug 28 '20 at 22:26
  • By similar, I meant that clang also tries to initialize `Var` with `directory_entry`. If it's fixed in trunk, should I consider that as a known compiler bug? Thanks for godbolt link. – Askold Ilvento Aug 28 '20 at 22:38
  • 1
    No, it isn't. But if you want to avoid that error, you should mark your constructor as explicit. – Mansoor Aug 28 '20 at 22:41
  • In other words, `directory_entry` does not have an explicit `ostream& operator<<` and thus conversation has to be used anyway. The compiler chooses my one, that is ill and SFINAE does not help. Explicit solves this issue. Trunk compiler version has ostream& operator<< for directory_entry, the compiler does not look for conversion, does not it? That explains most of the questions. Why friend makes a workaround? – Askold Ilvento Aug 28 '20 at 22:58

1 Answers1

0

With Mansoor's hints, I think I found out.

C-tor

This code is dangerous.

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

Avoid this. The compiler will try to substitute such c-tor in any implicit conversion where Var is visible and may be suitable. Mark c-tor as explicit is the most straightforward way to restrict such wild substitution. SFINAE and enable_if can be other ways to restrict substitutions.

And the ill substitution is exactly what breaks complication in my case because in old complies directory_entry does not have a direct definition of ostream<<. The compiler looks for a converter and finds suitable Var. Which can be instantiated but can not be complied. The latter is good because if it could, the error was untraceable.

There is a patch https://gcc.gnu.org/legacy-ml/gcc-patches/2018-06/msg01084.html about 2018 (or maybe it is soon fixed on it) that introduced explicit ostream << directory_entry.

Before that directory_entry could be converted to path with implicit c-tor like that

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

That is why ostream << directory_entry works if no ostream<< is explicitly defined.

Friend

Friend actually scopes converter visibility. That works also

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

But if std::ostream& is declared as friend and definition (or other declaration) is visible for ostream << directory_entry when it breaks complication again because of other declaration is visible for wild substitution. That explains why separation into several units and using friend created a workaround.

SFINAE

SFINAE does not check the function body. It works only with declarations. _value{value} is body. To invoke SFINAE c-tor should be like

  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} {
  }

Brilliant ideas how to deal with std::variant depended c-tor can be dug out here: How do I check if an std::variant can hold a certain type .

Askold Ilvento
  • 1,405
  • 1
  • 17
  • 20