1

I have this Java code:

public interface Adapter<FROM,TO> {
    /**
     * Adapts something from one type to a type. 
     * @param f
     * @return
     */
    public TO adapt(FROM f);
    
    /**
     * Chains adapters. 
     * 
     * @param <NEXT>
     * @param next 
     * @return a new adapter that takes the output of this adapter and 
     * adapts to something of type NEXT.
     */
    public default <NEXT> Adapter<FROM,NEXT> chain(Adapter<TO,NEXT> next){
        Adapter<FROM,TO> x=this; 
        return new Adapter<FROM,NEXT>(){
            @Override
            public NEXT adapt(FROM f) {
                return next.adapt(x.adapt(f));
            }
        };
    }

I'd really like something similar in C++. The best I've done so far is:

#ifndef ADAPTER_H
#define ADAPTER_H
 
template <typename FROM, typename TO>
class Adapter
{
  public:
  TO adapt(FROM f);
 
  template <typename NEXT> Adapter<FROM, NEXT> chain(Adapter<TO, NEXT> arg)
  {
    class UnNamed: public Adapter<FROM, NEXT>
    {
      public: 
      Adapter<TO, NEXT> outer;
      UnNamed(Adapter<TO, NEXT> arg)
      {
        outer = arg;
      }
      NEXT adapt(FROM from)
      {
         outer.adapt(Adapter::adapt(from));
      }
     };
     return UnNamed(arg);
   }
};
 
#endif

but it fails with undefined reference errors. I'd really like to keep the api as close to the Java as possible, but I can't figure how to get the nested class to exist in a referenceable way.

Dale
  • 333
  • 4
  • 11

2 Answers2

6

You are close.

In Java, interfaces have to be implemented by concrete classes, and interface methods are virtual and must be overriden in those classes. In the case of your example, the use of the default keyword on the chain() method allows Java to generate its own class to implement the Adapter interface that chain() returns.

In C++, you can do something similar, except that there is no equivalent to Java's default keyword (C++ has a default keyword, but its meaning is very different), so you need to explicitly define your own class type to implement your virtual interface. You tried doing that, however polymorphism doesn't work unless you call the virtual methods via pointers/references to objects, otherwise you can suffer from object slicing. In a way, Java also enforces this rule, because all objects in Java are reference types, only fundamental types are value types (whereas in C++, classes can also be used as value types).

Try something more like this instead:

#include <memory>

template<typename FROM, typename TO>
class Adapter
{
public:
    /**
     * Adapts something from one type to a type. 
     * @param f
     * @return
     */
    virtual TO adapt(FROM f) = 0;
    
    /**
     * Chains adapters. 
     * 
     * @param <NEXT>
     * @param next 
     * @return a new adapter that takes the output of this adapter and 
     * adapts to something of type NEXT.
     */
    template<typename NEXT>
    std::unique_ptr<Adapter<FROM, NEXT>> chain(Adapter<TO, NEXT> *next)
    {
        class ChainedAdapter : public Adapter<FROM, NEXT>
        {
        private:
            Adapter<FROM, TO> *x; 
            Adapter<TO, NEXT> *next;
        public:
            ChainedAdapter(Adapter<FROM, TO> *x, Adapter<TO, NEXT> *next)
                : x(x), next(next) {}

            NEXT adapt(FROM f) override {
                return next->adapt(x->adapt(f));
            }
        };

        return std::make_unique<ChainedAdapter>(this, next);
    }
};

Online Demo

Although, I suppose you could also get away with making chain() return a new ChainedAdapter without creating it dynamically, if you are very careful with the returned object and don't use it in any way that would slice it:

#include <memory>
     
template<typename FROM, typename TO>
class Adapter
{
public:
    /**
     * Adapts something from one type to a type. 
     * @param f
     * @return
     */
    virtual TO adapt(FROM f) = 0;
     
    /**
     * Chains adapters. 
     * 
     * @param <NEXT>
     * @param next 
     * @return a new adapter that takes the output of this adapter and 
     * adapts to something of type NEXT.
     */
    template<typename NEXT>
    auto chain(Adapter<TO, NEXT> *next)
    {
        class ChainedAdapter : public Adapter<FROM, NEXT>
        {
        private:
            Adapter<FROM, TO> *x; 
            Adapter<TO, NEXT> *next;
        public:
            ChainedAdapter(Adapter<FROM, TO> *x, Adapter<TO, NEXT> *next)
                : x(x), next(next) {}
     
            NEXT adapt(FROM f) override {
                return next->adapt(x->adapt(f));
            }
        };
     
        return ChainedAdapter(this, next);
    }
};

Online Demo

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
3

I'd avoid direct Java code translation.

One C++ way is to embrace values.

template<class Sig>
struct adapter;
template<class Out, class In>
struct adapter<Out(In)>:std::function<Out(In)>
{
  using std::function<Out(In)>::function;
  template<class Next, class Result=std::invoke_result_t<Next const&, Out>>
  adapter<Result(In)> chain( Next next ) const& {
    auto tmp=*this;
    return std::move(tmp).chain(std::move(next));
  }
  template<class Next, class Result=std::invoke_result_t<Next, Out>>
  adapter<Result(In)> chain( Next next ) && {
    return [self=std::move(*this), next=std::move(next)](In in)->Result {
      return next(self(std::forward<In>(in)));
    };
  }
};

there we go.

adapter<int(double)> rounder=[](double d){return std::floor(d);};
adapter<double(std::istream&)> double_reader=[](auto&is){double d; is>>d; return d;};
adapter<int(std::istream&)> int_reader=double_reader.chain(rounder);
std::cout << int_reader(std::cin);

etc.

These adapters are polymorphic value types. And you can chain them with function pointers, lambdas, std functions, or other function objects

The x.adapt(foo) call is spelled x(foo) in the above code. Objects whose primary purpose is to be called ... can use operator() to make then callable.

Live example.

Design:

I made std::function do most of the heavy lifting. It is a polymorphic value type that supports calling and nullability.

We simply inherit from it, forward constructors, and add in a .chain method that checks compatibility and deduces the return type.

We can extend adapters to support many-one adaption easily. The first step is to support multiple inputs just by adding a bunch of ...s in the right spot:

template<class Sig>
struct adapter;
template<class Out, class... In>
struct adapter<Out(In...)>:std::function<Out(In...)>
{
    using std::function<Out(In...)>::function;
    template<class Next, class Result=std::invoke_result_t<Next const&, Out>>
    adapter<Result(In...)> chain( Next next ) const& {
        auto tmp=*this;
        return std::move(tmp).chain(std::move(next));
    }
    template<class Next, class Result=std::invoke_result_t<Next, Out>>
    adapter<Result(In...)> chain( Next next ) && {
        return [self=std::move(*this), next=std::move(next)](In... in)->Result {
            return next(self(std::forward<In>(in)...));
        };
    }
};

Now, the first.chain(second) syntax doesn't work as well with multiple inputs for second. We can add another method:

template<class Sig>
struct adapter;
template<class Out, class... In>
struct adapter<Out(In...)>:std::function<Out(In...)>
{
    using std::function<Out(In...)>::function;
    template<class Next, class Result=std::invoke_result_t<Next const&, Out>>
    adapter<Result(In...)> chain( Next next ) const& {
        auto tmp=*this;
        return std::move(tmp).chain(std::move(next));
    }
    template<class Next, class Result=std::invoke_result_t<Next, Out>>
    adapter<Result(In...)> chain( Next next ) && {
        return [self=std::move(*this), next=std::move(next)](In... in)->Result {
            return next(self(std::forward<In>(in)...));
        };
    }
    template<class...First>
    adapter<Out(First...)> consume( adapter<In(First)>... src)&&
    {
        return [self=std::move(*this), ...src=std::move(src)](First... first)->Out
        {
            return self(src(std::forward<First>(first))...);
        };
    }
    template<class...First>
    adapter<Out(First...)> consume( adapter<In(First)>... src) const&
    {
        auto tmp = *this;
        return std::move(tmp).consume( std::move(src)... );
    }
};

but that is going a bit far, no?

Live example.

There are some who are leery about inheriting from a value type (like std::function) in a non-polymorphic manner. Here, I'd argue that almost nobody in their right mind stores pointers to std::functions; those that do, often are doing a shared_ptr trick (which handles polymorphic destruction).

However, if you are concerned, you can do this:

template<class Out, class... In>
struct adapter<Out(In...)>
{
  using F = std::function<Out(In...)>;
  F f;
  // forward call operator
  template<class...Args>
  auto operator()(Args&&...args)const
  -> std::invoke_result_t<F const&, Args&&...>
  {
    return f(std::forward<Args>(args)...);
  }
  // default all special member functions:
  adapter()=default;
  adapter(adapter const&)=default;
  adapter(adapter &&)=default;
  adapter& operator=(adapter const&)=default;
  adapter& operator=(adapter &&)=default;
  ~adapter()=default;
  // forward a few useful operations and ctors:
  explicit operator bool() const { return (bool)f; }
  template<class Fin> 
  requires (!std::is_same_v<Fin, adapter> && std::convertible_to<Fin, F>)
  adapter( Fin fin ):f(std::forward<Fin>(fin)) {}

then add in the .chain methods.

As you can see, this adds a fair bit of code. Live example.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I doubt that inheriting from `std::function` really reflects C++ value – Guillaume Racicot Aug 05 '21 at 02:38
  • @GuillaumeRacicot Inheriting a value type from another value type is a perfectly good idea, especially when slicing isn't a problem; the std function slice of the adapter above is *correct*. Only if you are storing owning dumb pointers to std function (... and why are you doing that) is there a problem (including owning unique ptrs without a destroy function; but again, a unique ptr to a std function is serious code smell.) – Yakk - Adam Nevraumont Aug 05 '21 at 02:48