【问题标题】:C++ templated function with templated class as return value以模板类作为返回值的 C++ 模板函数
【发布时间】:2021-10-10 00:25:12
【问题描述】:

我有这个 Java 代码:

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

我真的很喜欢 C++ 中类似的东西。到目前为止我做得最好的是:

#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

但由于未定义的引用错误而失败。我真的很想让 api 尽可能接近 Java,但我不知道如何让嵌套类以可引用的方式存在。

【问题讨论】:

标签: c++ templates undefined-reference


【解决方案1】:

你很接近。

在 Java 中,interfaces 必须由具体类实现,而interface 方法是虚拟的,必须在这些类中被覆盖。在您的示例中,在 chain() 方法上使用 default 关键字允许 Java 生成自己的类来实现 chain() 返回的 Adapter 接口。

在C++中,你可以做类似的事情,只是没有等价于Java的default关键字(C++有一个default关键字,但是它的含义很不一样),所以你需要显式定义你自己的类类型来实现你的虚拟接口。您尝试这样做,但是多态性不起作用,除非您通过对象的指针/引用调用虚拟方法,否则您可能会遭受object slicing 的困扰。在某种程度上,Java 也强制执行了这个规则,因为 Java 中的所有对象都是引用类型,只有基本类型是值类型(而在 C++ 中,类也可以用作值类型)。

尝试类似的方法:

#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

虽然,我想你也可以让chain()返回一个新的ChainedAdapter而不动态创建它,如果你对返回的对象非常小心并且不要以任何方式使用它会切片它:

#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

【讨论】:

    【解决方案2】:

    我会避免直接翻译 Java 代码。

    一种 C++ 方式是拥抱价值观。

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

    我们去。

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

    等等

    这些适配器是多态值类型。您可以将它们与函数指针、lambda、std 函数或其他函数对象链接起来

    x.adapt(foo) 调用在上面的代码中拼写为x(foo)。主要目的是被调用的对象......可以使用operator() 使 then 可调用。

    Live example.

    设计:

    我让std::function 完成了大部分繁重的工作。它是一种支持调用和可空性的多态值类型。

    我们只是从它继承,转发构造函数,并添加一个检查兼容性并推断返回类型的.chain 方法。

    我们可以扩展适配器以轻松支持多合一适配。第一步是支持多个输入,只需在正确的位置添加一堆...s:

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

    现在,first.chain(second) 语法不适用于second 的多个输入。我们可以添加另一个方法:

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

    但这有点远,不是吗?

    Live example.

    有些人对以非多态方式从值类型(如std::function)继承持怀疑态度。在这里,我认为几乎没有人在他们的正常头脑中存储指向std::functions 的指针;那些这样做的人,通常是在使用shared_ptr 技巧(处理多态破坏)。

    但是,如果您担心,您可以这样做:

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

    然后添加.chain 方法。

    如您所见,这增加了相当多的代码。 Live example.

    【讨论】:

    • 我怀疑从std::function继承真的体现了C++价值
    • @GuillaumeRacicot 从另一个值类型继承一个值类型是一个非常好的主意,尤其是在切片不成问题的情况下;上面适配器的std函数切片是正确的。只有当您存储拥有指向 std 函数的哑指针(......以及为什么要这样做)时才会出现问题(包括拥有没有破坏函数的唯一 ptr;但同样,std 函数的唯一 ptr 是严重的代码气味.)
    猜你喜欢
    • 2018-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多