Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is a lazy "operator or" overload possible with placement/piecewise syntax?

Tags:

So we have all been there. I have a nice object, I have a constructor for it, the object can resolve true if it is created correctly, but it also has other state, so I can happily write:

if (const auto& str = std::ofstream("/tmp/myfile"))
{
  str << "Oh that was easy\n";
}

But what I would really like to do is try a few alternates:

if (const std::stream& str = (std::ofstream("/tmp/myfile") || std::ofstream("/temp/myfile") || std::ofstream("./myfile")) )
{
  str << "Oh that was not so easy\n";
}

Clearly not! I potentially opened 3 files, but cast them to "true" bool, and couldn't cast back to std::stream, of course. If I overload operator || for stream rvals then it opens all 3 files because the lazy evaluation is forgotten!

I know, std::stream is perhaps a poor example, but 20 pages later I will have finished explaining my option string processor, so lets stick with std::stream.

What I would expect might be possible would be something like:

if (const std::stream str = (std::piecewise_construct(std::ofstream,"/tmp/myfile") || std::piecewise_construct(std::ofstream,"/temp/myfile") || std::piecewise_construct(std::ofstream,"./myfile")) )
{
  str << "Oh that maybe makes sense?\n";
}

To do this it looks like I might need 2 overloads of operator ||, one that takes two unsolved piecewise_construct and resolves the left one to get started, and another that takes an rval object on the left and only evaluates the right hand side if it is "failed". Something like:

template<class PC2>
std::stream operator ||(std::stream&& str, const PC2& pc2)
{
  if (str)
    return std::move(str);
  return pc2.construct();
}

template<class PC1, class PC2>
std::stream operator ||(const PC1& pc1, const PC2& pc2)
{
  return operator||(pc1.construct(), pc2);
}

Where PCx here is the piecewise construct.

But I can see straight away that std::peicewise_construct is just a tag. It doesn't itself have any functionality. Also I am fighting to find any way I can make this work with template FINAE restrictions: Not much point creating two unrelated types that can't be moved. There are a number of similar "tricks" floating around in C++, such as std::function or lambdas that look to do a similar job?

Right now I would love a solution in C++11 that doesn't look like Frankenstein after a fight with his monster, but don't feel restricted if the feature I want is just a version away!

Meanwhile I have 3 copies of basically the same argument value parser and storage because customers can't agree what the option should be called between versions.

I guess I'm after some sort of customisable template, rather than something hardwired to the specific class, in this case std::stream, and even stream has numerous constructors. A common enough pattern with handles is code that basically want to do h = (open(O_APP) || open(O_CREATE) || open(O_TRUNC)) {except that is C, not objects}.

One way forward is some sort of templated class that holds the std::bind and knows how to construct, in turn built from a helper function. And perhaps the type of that object will allow the type protection that I also desire.

And another note... In the example, I am using std::istream with just 1 argument, but of course the constructor for istream can take a number of arguments and has a number of alternate constructors, and my internal usecase has at least 3 and probably more options in future, so it really needs to support that arbitrary argument list template stuff we all love.

like image 702
Gem Taylor Avatar asked Oct 15 '19 15:10

Gem Taylor


2 Answers

The simplest implementation is probably to write a function that takes a range of filenames and returns the first one that successfully opens:

std::ofstream first_file_of(std::initializer_list<std::string> names) {
    for (auto&& name : names) {
        std::ofstream os(name);
        if (os) return os;
    }
    return std::ofstream();
}

As in:

first_file_of({"/tmp/myfile", "/temp/myfile", "./myfile"})

But if you really want to go crazy with operators, you need some object that has some overloaded binary operator that performs this kind of conditional.

One such implementation, using operator|, would be:

struct or_else_file {
    std::string name;

    or_else_file(std::string name) : name(name) { }

    friend std::ofstream operator|(std::ofstream&& os, or_else_file oef) {
        if (os) {
            return std::move(os);
        } else {
            return std::ofstream(oef.name);
        }
    }
};

As in:

std::ofstream("/tmp/myfile") | or_else_file("/temp/myfile") | or_else_file("./myfile")

This is more complicated, and more verbose, but could be said to read better depending on your perspective.

like image 101
Barry Avatar answered Sep 29 '22 00:09

Barry


Try this:

template<class Tuple, std::size_t...Is>
struct try_make_t {
  Tuple inputs;
  template<std::size_t I, class T>
  bool try_make( std::optional<T>& out ) {
    out.emplace( std::get<I>(inputs) );
    return (bool)*out;
  }
  template<class T>
  operator T()&&{
    std::optional<T> retval;
    ( try_make<Is>( retval ) || ... );
    if (!retval)
      return {};
    return std::move(*retval);
  }
};
template<class...Ins, std::size_t...Is>
try_make_t< std::tuple<Ins&&...>, Is... > try_make_helper( std::index_sequence<Is...>, Ins&&...ins ) {
  return {std::tuple<Ins&&...>( std::forward<Ins>(ins)... )};
}
template<class...Ins>
auto try_make_from( Ins&&...ins ) {
  return try_make_helper( std::make_index_sequence<sizeof...(ins)>{}, std::forward<Ins>(ins)... );
}
int main() {
  if (std::ofstream&& str = try_make_from("/tmp/myfile", "/temp/myfile", "./myfile") )
  {
    str << "Oh that was easy\n";
  }
}

for fancier construction

template<class F>
struct factory {
  F f;
  template<class T,
    std::enable_if_t< std::is_convertible_v< decltype(std::declval<F&>()()), T>, bool> =true
  >
  operator T()&& { return f(); }
  template<class T,
    std::enable_if_t< std::is_convertible_v< decltype(std::declval<F&>()()), T>, bool> =true
  >
  operator T()&& { return f(); }
  template<class T,
    std::enable_if_t< !std::is_convertible_v< decltype(std::declval<F&>()()), T>, bool> =true
  >
  operator T()&& { return std::make_from_tuple<T>(f()); }
};

where factory{ []{ return something; } } will create an arbitrary object from the return value of a lambda or from the tuple returned from a lambda.

if(
  std::ofstream&& file = try_make_from(
    factory{[]{return "/tmp/myfile";}},
    factory{[]{return std::make_tuple("/temp/myfile");}},
    "./myfile"
  )
)
like image 36
Yakk - Adam Nevraumont Avatar answered Sep 28 '22 23:09

Yakk - Adam Nevraumont