4

Here's a fun challenge.

I wanted to created some boilerplate that allows the clean handling of precondition failures using syntax similar to perl's x or die("reason") approach.

I came up with this deleter:

struct do_exit {
    [[noreturn]]
    void operator()(void*) const noexcept {
        std::exit(code_);
    }
    int code_;
};

Which we can use to manage the "deletion" of a temporary std::unique_ptr which is maybe pointing at std::cerr:

struct exit_stream
{
    exit_stream(int code, std::ostream& os)
    : stream_(std::addressof(os), do_exit { code })
    {}

    std::ostream& as_stream() const { return *stream_; }
    operator bool() const { return bool(*stream_); }

    template<class T>
    friend auto operator<<(const exit_stream& es, T const& t) -> exit_stream const&
    {
        es.as_stream() << t;
        return es;
    }

    std::unique_ptr<std::ostream, do_exit> stream_;
};

Created with the following function:

exit_stream die(int code = 100)
{
    return exit_stream(code, std::cerr);
}

Now we can write the following program:

int main(int argc, char* argv[]) {

    // preconditions
    argc >= 2 or die(4) << "usage: myprog <inputfile>\n";

    auto input = std::ifstream(argv[1]);
    input or die(100) << "failed to open " << argv[1] << "\n";
}

This works because when the exit_stream is returned as a temporary, it must be moved (unique_ptr deletes the copy operator). unique_ptr guarantees that the moved-from pointer will be null. It also guarantees that the deleter will only be called if the unique_ptr is not null. Therefore, exit will only be called after all streaming operations have been executed at the call site.

However, the obvious failing is that I can't write:

auto input = std::ifstream(argv[1]]) or die() << "failed to open " << argv[1] << "\n";

Because if I did, input would be a bool.

Here's my question. Can you think of a way to produce the required effect, or as close to it as possible?

I really want to avoid this, for reasons of efficiency in the passing case, and for clarity and elegance:

auto input = (std::ifstream(argv[1]]) or die() << "failed to open " << argv[1] << "\n").get();

Full code:

#include <fstream>
#include <iostream>
#include <cstdlib>

struct do_exit {
    [[noreturn]]
    void operator()(void*) const noexcept {
        std::exit(code_);
    }
    int code_;
};

struct exit_stream
{
    exit_stream(int code, std::ostream& os)
    : stream_(std::addressof(os), do_exit { code })
    {}

    std::ostream& as_stream() const { return *stream_; }
    operator bool() const { return bool(*stream_); }

    template<class T>
    friend auto operator<<(const exit_stream& es, T const& t) -> exit_stream const&
    {
        es.as_stream() << t;
        return es;
    }

    std::unique_ptr<std::ostream, do_exit> stream_;
};

exit_stream die(int code = 100)
{
    return exit_stream(code, std::cerr);
}

int main(int argc, char* argv[]) {

    // preconditions
    argc >= 2 or die(4) << "usage: myprog <inputfile>\n";

    auto input = std::ifstream(argv[1]);
    input or die(100) << "failed to open " << argv[1];
}
8
  • 2
    Considered overloading ||? Commented Mar 24, 2017 at 1:38
  • @Yakk I think the problem (at least in this approach) with overloading operator|| is that the objects must exists, because short-circuiting won't work. So the whole design gets broken. The most problematic part seems that operator<< will get evaluated. And it's quite hard to defer it, if possible at all. Commented Mar 24, 2017 at 3:01
  • @luk32 deferring should be easy to implement via saving operator<<'s arguments in a field of exit_stream insteaf of immediate printing. Commented Mar 24, 2017 at 5:11
  • Overloading || will get rid of the short circuiting behaviour. Commented Mar 24, 2017 at 10:59
  • @Simple but look out for that breaking change in c++17, after which it won't... Commented Mar 24, 2017 at 11:05

2 Answers 2

2

OK, here's my first attempt. I'm a little concerned about redundant construction because of the lack of short-circuiting, but the syntax is what I wanted.

Comments / improvements welcome!

#include <fstream>
#include <iostream>
#include <utility>
#include <iomanip>

namespace detail {
    struct tuple_emitter {
        tuple_emitter(std::ostream &os) : os(os) {}

        template<class...Refs>
        void operator()(std::tuple<Refs...> const &t) const {
            emit_impl(t, std::make_index_sequence<sizeof...(Refs)>());
        }

    private:

        template<class T>
        void emit_impl(T const &t) const {
            os << t;
        }

        // make sure we handle lazy manipulators
        void emit_impl(std::ios_base (* f)(std::ios_base &)) const
        {
            f(os);
        }

        template<class Tie, std::size_t...Is>
        void emit_impl(Tie const &refs, std::index_sequence<Is...>) const {
            using expand = int[];

            void(expand{0,
                        ((emit_impl(std::get<Is>(refs))), 0)...});
            os << std::endl;
        };


        std::ostream &os;
    };

    // a class that emits a number of references to std::cerr and then
    // exits    
    template<class...Refs>
    struct dier {
        dier(int code, std::tuple<Refs...> t) : code(code), emitter(std::cout), refs_(t) {}

        template<class...Others, class Ref>
        dier(dier<Others...> const &original, Ref const &r)
                : code(original.code), emitter(original.emitter), refs_(std::tuple_cat(original.refs_, std::tie(r))) {};

        void operator()() const {
            emitter(refs_);
            std::exit(code);
        }

        int code;
        tuple_emitter emitter;
        std::tuple<Refs&...> refs_;
    };

    template<class...Refs, class Ref>
    auto operator<<(dier<Refs...> original, Ref const &ref) {
        return dier<Refs..., const Ref &>(original, ref);
    };


    template<class T, class...Refs>
    T operator||(T &&t, dier<Refs...> death) {
        if (!t) {
            death();
        }
        return std::move(t);
    };

    template<class T, class...Refs>
    T const &operator||(T &t, dier<Refs...> death) {
        if (!t) {
            death();
        }
        return t;
    };
}

auto die(int code = 100) {
    return detail::dier<>(code, std::make_tuple());
}

int main(int argc, char *argv[]) {

    // preconditions
    argc >= 2 or die(4) << "usage: myprog <inputfile>\n";

    auto input = std::ifstream(argv[1]) or die(5) << "failed to open " << std::setfill('-') << std::setw(10) << argv[1] << " and here is a hex number " << std::hex << 40;

}

Expected outputs:

With no arguments:

usage: myprog <inputfile>

With one argument "foo.txt":

failed to open ---foo.txt and here is a hex number 28

Update after Yakk's suggestions

got it down to this - which is much cleaner:

#include <fstream>
#include <iostream>
#include <utility>
#include <iomanip>

namespace notstd {

    namespace detail {
        template<class F, class T, std::size_t...Is>
        void apply_impl(F&& f, T&& t, std::index_sequence<Is...>)
        {
            using expand = int[];
            void(expand{0,
                        ((f(std::get<Is>(t))), 0)...});
        };
    }

    template<class F, class...Ts>
    void apply(F&& f, std::tuple<Ts...> const& t)
    {
        detail::apply_impl(std::forward<F>(f), t, std::make_index_sequence<sizeof...(Ts)>());
    };

    struct apply_to_stream
    {
        apply_to_stream(std::ostream& os)
                : os(os)
        {}

        template<class T>
        std::ostream& operator()(T&& t) const
        {
            return os << t;
        };

        std::ostream& operator()(std::ios_base& (*f)(std::ios_base&)) const
        {
            f(os);
            return os;
        };

        std::ostream& os;
    };


}

namespace detail {

    template<class Tuple>
    struct dier {
        constexpr dier(int code, Tuple t) : code(code), refs_(std::move(t)) {}

        void operator()() const {
            notstd::apply(notstd::apply_to_stream(std::cout), refs_);
            std::cout << '\n';
            std::exit(code);
        }

        int code;
        Tuple refs_;
    };

    template<class Tie>
    constexpr auto make_dier(int code, Tie&& t) {
        return detail::dier<Tie>(code, std::forward<Tie>(t));
    }

    template<class Tuple, class Ref>
    constexpr auto operator<<(dier<Tuple> original, Ref&& ref) {
        return make_dier(original.code, std::tuple_cat(original.refs_, std::tie(ref)));
    }


    template<class T, class...Refs>
    T operator||(T &&t, dier<Refs...> death) {
        if (!t) {
            death();
        }
        return std::forward<T>(t);
    }

}

template<class Tie = std::tuple<>>
constexpr auto die(int code = 100, Tie&& t = {}) {
    return detail::make_dier(code, std::forward<Tie>(t));
}

int main(int argc, char *argv[]) {

    // preconditions
    argc >= 2 or die(4) << "usage: myprog <inputfile>\n";

    auto input = std::ifstream(argv[1]) or die(5) << "failed to open " << std::setfill('-') << std::setw(10) << argv[1] << " and here is a hex number " << std::hex << 40;
}
Sign up to request clarification or add additional context in comments.

4 Comments

template<class T, class...Refs>T operator||(T &&t, dier<Refs...> death) { if (!t) { death(); } return std::forward<T>(t); }; and no need for 2 overloads.
Template dier on tuple stored, not on args. Write factory that takes tuple and deduces dier type. Feed it with tuple cat. Use generic foreach(f)(args...) and a C++17-esque apply to stream, cut out special purpose code from emitter.
This is one of the cases where std::endl seems wise, before calling exit, flush those buffers! ;)
@Yakk I think you're right. The documentation for std::exit only promises to flush C streams. It probably works anyway in libc++ and stdlibc++ but only by coincidence.
1

This is my attempt to improve @RichardHodges self answer.

First, some library esque code:

namespace notstd {
  // takes a function object
  // returns a function object that calls the first function object once for each argument
  template<class F>
  auto foreach( F&& f ) {
    return [f=std::forward<F>(f)](auto&&...args)mutable{
      using discard=int[];
      (void)discard{0,(void(
        f(decltype(args)(args))
      ),0)...};
    };
  }
  // weak version of std::apply:
  template<std::size_t...Is, class F, class Tuple>
  decltype(auto) apply( std::index_sequence<Is...>, F&& f, Tuple&& tup) {
    return std::forward<F>(f)( std::get<Is>(std::forward<Tuple>(tup))... );
  }
  template<class F, class Tuple>
  decltype(auto) apply(F&& f, Tuple&& tup) {
    auto indexes = std::make_index_sequence< std::tuple_size< std::remove_reference_t<Tuple> >{} >{};
    return apply(indexes, std::forward<F>(f), std::forward<Tuple>(tup) );
  }
}

now we use it. This code is briefer due to the above:

namespace death {
  namespace details {
    template<class Tuple>
    struct dier;
  }

  template<class Tuple=std::tuple<>>
  details::dier<Tuple> die( int code, std::ostream& os = std::cerr, Tuple&& tuple={} ) {
    return {code, os, std::forward<Tuple>(tuple)};
  }

  namespace details {
    // a class that emits a number of references to std::cerr and then
    // exits  
    template<class Tuple>
    struct dier {
      void operator()() const {
        notstd::apply( notstd::foreach( [&](auto const&e){ os << e; } ), refs_ );
        os << std::endl;
        std::exit(code);
      }

      int code;
      std::ostream& os;
      Tuple refs_;
      template<class Ref>
      friend auto operator<<(dier&& original, Ref const &ref) {
        return die( original.code, original.os,  std::tuple_cat( std::move(original).refs_, std::tie(ref) ) );
      }
      template<class T>
      friend T operator||(T &&t, dier&& death) {
        if (!t) {
          death();
        }
        return std::forward<T>(t);
      }
    };
  }
}

auto die(int code = 100, std::ostream& os = std::cerr) {
  return death::die(code, os);
}

Hope that helps.

Live example.

Note that all ... is eliminated except in the utility code.

3 Comments

nice suggestions - thank you. In my update I have done away with the emitter and instead created a function object called apply_to_stream (which I think has many other uses)
@RichardHodges Yes, eliminating it does clean it up. I didn't bother with apply_to_stream, that is just apply( foreach([&](auto&& e){os<<e;}, tuple ).
Oh you're right. Why did I think I had to specialise for the unary manipulator? Nice.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.