【问题标题】:C++ generic class dealing with dereferencing syntax处理解引用语法的 C++ 泛型类
【发布时间】:2017-01-12 14:18:58
【问题描述】:

处理某些类型需要使用 .运算符,而其他人使用 -> 运算符。

最好为 .运算符并让调用者包装类型,如下面的代码示例所示。

来自 C# 背景,我不习惯遇到这个特殊问题。

#include <iostream>
#include <string>
#include <vector>
#include <memory>

template<class T>
class container{
  public:
    void add(T element){
        elements_.push_back(std::move(element));   
    }

    void process(){
        for(auto& a: elements_){
            a.print();   
        }
    }
  private:
    std::vector<T> elements_;
};

class printable{
public:
    void print(){
        std::cout << "Print\n";   
    }
};

template<class T>
class printable_forwarder{
public:
    printable_forwarder(T element): element_{std::move(element)}{

    }

    void print(){
        element_->print();   
    }

private:
    T element_;
};

int main()
{
    container<printable> c1;
    c1.add(printable{});
    c1.process();

   container<printable_forwarder<std::shared_ptr<printable>>> c2;
   std::shared_ptr<printable> sp{std::make_shared<printable>()};
   c2.add(printable_forwarder<decltype(sp)>{sp});
   c2.process();
}

这样看起来更好吗?

#include <iostream>
#include <string>
#include <memory>
#include <type_traits>
#include <vector>
template<typename T>
class dereference
{
public:
    inline static T& get(T& value){
        return value;
    }
};

template<typename T>
class dereference<T*>
{
public: 
    inline static typename std::add_lvalue_reference<typename std::remove_pointer<T>::type>::type get(T* value){
        return *value;
    }
};

template<typename T>
class dereference<std::shared_ptr<T>>
{
public: 
    inline static T& get(std::shared_ptr<T> value){
        return *value.get();
    }
};

template<class T>
class container{
public:
    void add(T const& v){
        items_.push_back(v);   
    }

    void print_all(){
        for(auto& a: items_){
            dereference<T>::get(a).print();   
        }
    }
private:
    std::vector<T> items_;
};

struct printable{
    void print(){
        std::cout << "Printing\n";   
    }
};

int main()
{
    container<printable> c1;
    c1.add(printable{});
    c1.print_all();

    container<std::shared_ptr<printable>> c2;
    c2.add( std::shared_ptr<printable>(new printable{}));
    c2.print_all();
}

【问题讨论】:

  • 你只需要等到我们能够重载operator.,就像在 C++54 中一样。有一个提案,我相信 Bjarne 参与其中。
  • 当你调用你的泛型函数/创建泛型对象或其他什么时,你不能只引用指针吗?无需过度设计。
  • 你不能因为你没有指定回调
  • 我不明白你要解决什么问题,但我不得不承认我也不明白你的解决方案;)。正如乔治所说,只需在调用函数之前取消引用指针......
  • 如果你创建一个容器> 例如打印调用失败。

标签: c++ templates generic-programming


【解决方案1】:

处理某些类型需要使用 .运算符,而其他人使用 -> 运算符。

别这样。

你的工作是写template&lt;class T&gt; class container。该容器包含Ts。如果您的用户想在T 上做某事,您应该公开做某事的能力 - 但正确执行该操作是他们的责任。否则,您只会添加大量代码膨胀。太好了,你给了我一种打印所有元素的方法,但是如果我知道在它们上调用foo() 或者找到bar() 返回大于42 的第一个元素怎么办?显然,你不会写for_each_foo()find_if_bar_is_42()

这就是标准库将容器与算法分开的原因。让你的容器尽可能可用的方法是让它通过begin()end() 暴露两个iterators,然后我就可以作为用户做任何我需要做的事情:

container<T> values;
values.add(...);

// I know to use '.'
for (T& t : values) {
   t.print();
} 

container<T*> pointers;
pointers.add(...);

// I know to use '->'
for (T* t : pointers) {
    t->print();
}

auto iter = std::find_if(pointers.begin(), pointers.end(), [](T* t){
    return t->bar() == 42;
});

除此之外,您可以添加一堆成员函数,这些函数本身可以调用,因此您可以将工作传递给用户:

template <class F>
void for_each(F&& f) {
    for (auto& elem : elements_) {
        f(elem);              // option a
        std::invoke(f, elem); // option b, as of C++17
    }
}

所以上面的例子是:

values.for_each([](T& t){ t.print(); });
pointers.for_each([](T* t){ t->print(); });
values.for_each(std::mem_fn(&T::print));
pointers.for_each(std::mem_fn(&T::print));

请注意,始终由用户决定该做什么。另外,如果你在for_each的实现中使用std::invoke(),那么你可以写:

pointers.for_each(&T::print);
values.for_each(&T::print);

而且,就此而言:

container<std::unique_ptr<T>> unique_ptrs;
unique_ptrs.for_each(&T::print);

【讨论】:

    【解决方案2】:

    作为另一个答案中建议的使用打印机类型参数化 container 的替代方法,我建议改为参数化 container::process() 方法:

    template<typename F>
    void process(F&& func)
    {
        for (auto& e : elements)
        {
            func(e);
        }
    }
    

    那么客户端代码将如下所示:

    container<printable> value_container;
    value_container.add(...);
    value_container.process([](printable& obj) { obj.print(); });
    
    container<printable*> ptr_container;
    ptr_container.add(...);
    ptr_container.process([](printable* obj) { obj->print(); });
    

    【讨论】:

      【解决方案3】:

      布莱尔,

      我认为更惯用的现代方法是使用 traits 类型。此模式允许库作者创建一个协议,该协议将由库在常见情况下实现,但可扩展,以便客户端可以支持任何必要的情况。

      在下面的代码中,我将 container 类放在名为 library 的命名空间中,并在名为 printable 的命名空间中定义了 printable 类em>客户。我还为了演示这种模式的客户端可扩展性,创建了一个名为 other_printable 的新客户端类型,它支持我们想要的功能(它打印),但有一个不同的 API(有一个独立的print,而不是成员函数print)。

      traits 类,print_traits,只是一个具有全部或部分特化的类型模板,一些由库提供,一些可能由客户端提供。在这种情况下,主模板有一个实现(它调用 print 成员函数)。有时在这种模式中,没有主要的实现,每个案例都是专门化的。

      库想要支持的用例是:

      1. 具有打印成员函数的类型
      2. 指向支持类型的指针
      3. std::unique_ptr 到支持的类型
      4. std::shared_ptr 到支持的类型

      因此,除了支持案例 1 的主模板外,库作者还为其他三种情况提供了特化(library 命名空间中的特化。)

      由于客户端想要使用不遵循库支持的 API 的类型(print 成员),客户端只需创建一个 print_traits 特化来处理不受支持的 API(独立的 print 函数)。

      请注意,通过添加这种特殊化,我们使 other_printable 成为受支持的类型,以便我们可以创建包含指向它的指针(包括智能指针)的容器。

      还要注意一个特化模板定义,但要与它特化的主模板在同一个命名空间中。这意味着客户端代码必须打开 library 命名空间来专门化 print_traits

      代码如下:

      #include <iostream>
      #include <vector>
      #include <memory>
      
      // as if included from library.hpp
      namespace library
      {
      template <class T>
      struct print_traits
      {
          static void print(T const& t)
          {
              t.print();
          }
      };
      
      template <class T>
      struct print_traits<T*>
      {
          static void print(T* p)
          {
              print_traits<T>::print(*p);
          }
      };
      
      template <class T>
      struct print_traits<std::unique_ptr<T>>
      {
          static void print(std::unique_ptr<T>const& p)
          {
              print_traits<T>::print(*p);
          }
      };
      
      template <class T>
      struct print_traits<std::shared_ptr<T>>
      {
          static void print(std::shared_ptr<T>const& p)
          {
              print_traits<T>::print(*p);
          }
      };
      
      
      template<class T>
      struct container
      {
          void insert(T element)
          {
              elements_.push_back(std::move(element));   
          }
      
          void process()
          {
              for (auto const& a: elements_)
              {
                  print_traits<T>::print(a);
              }
          }
        private:
          std::vector<T> elements_;
      };
      }
      
      // as if included from client.hpp (which would include library.hpp)
      namespace client
      {
          struct printable
          {
              void print() const
              {
                  std::cout << "Print\n";
              }
          };
      
          struct other_printable {};
      
          void print(other_printable const&op)
          {
              std::cout << "Print\n";
          }
      
      }
      
      // template specializations must be in the same namespace as the primary
      namespace library
      {
          template <>
          struct print_traits<client::other_printable>
          {
              static void print(client::other_printable const& op)
              {
                  client::print(op);
              }
          };
      }
      
      // main.cpp includes client.hpp
      int main()
      {
          using client::printable;
          using client::other_printable;
          using library::container;
      
          printable p0;
      
      
          container<printable> c0;
          c0.insert(p0);
          c0.process();
      
          container<printable*> c1;
          c1.insert(&p0);
          c1.process();
      
          container<std::unique_ptr<printable>> c2;
          c2.insert(std::make_unique<printable>());
          c2.process();
      
          container<std::shared_ptr<printable>> c3;
          c3.insert(std::make_shared<printable>());
          c3.process();
      
          other_printable op;
      
          container<other_printable> c4;
          c4.insert(op);
          c4.process();
      
          container<std::unique_ptr<other_printable>> c5;
          c5.insert(std::make_unique<other_printable>());
          c5.process();
      
      }
      

      我不得不指出这种事情在 C++ 中并不经常出现,因为我们通常不希望以相同的方式处理对象和指向它们的事物。也就是说,我希望这展示了一种可用于在特定情况下实现这一目标的方法。

      【讨论】:

      • 你的意思是“我们通常不希望以同样的方式对待对象和指向它们的事物”
      • 另外,如果你要设计一个替代解决方案,你会推荐什么。
      • 在 std::is_pointer::value ?? 的意义上不是真正的特征
      • @BlairDavidson 您似乎有一个非常具体的用例(带有print 函数的容器),但出于某种原因,您希望您的容器与printableshared_ptr&lt;printable&gt; 兼容.我本来希望您的特定用例需要其中一个。为什么需要同时支持两者?
      • @BlairDavidson “从 std::is_pointer::value 的意义上说,这并不是一个真正的特征??” traits 类模式的定义特征不是 trait 的类型,而是引入了支持自定义的第三个元素(trait 类),以便类型(两者都未修改)可以相互工作。之所以可以这样做,是因为这两种类型(库)中的一种是在设计时考虑了特征类。我们通常将特征类用于类型定义这一事实并不妨碍我们将其用于功能。
      【解决方案4】:

      这只是一个警告,试图自动化该过程以检查容器存储的类型是否是恰好是实际智能指针的指针类型。此警告来自 boost 文档:

      重要的 is_pointer 仅检测“真实”指针类型,而不检测智能指针。用户不应将 is_pointer 专门用于智能指针类型,因为这样做可能会导致 Boost(和其他第三方)代码无法正常运行。想要检测智能指针的特征的用户应该创建自己的特征。但是,请注意,通常无法自动检测智能指针类型,因此必须针对每种支持的智能指针类型部分专门化此类特征。

      可以找到here。我认为这通常与手头的问题有关,因为它并不能完全回答这个问题,它只是提示或提前知道的好东西,以帮助设计源代码的决策过程。

      【讨论】:

        【解决方案5】:

        我不确定这是否是最好的解决方案,但您可以对一个函数进行这四个重载:

        template<typename T>
        T& dereference(T& obj) {
            return obj;
        }
        
        template<typename T>
        T& dereference(std::shared_ptr<T> obj) {
            return *obj;
        }
        
        template<typename T>
        T& dereference(std::unique_ptr<T> obj) {
            return *obj;
        }
        
        template<typename T>
        T& dereference(T* obj) {
            return *obj;
        }
        

        现在您可以将任何对象、智能指针(已弃用的std::auto_ptr 除外)或原始指针传递给它:

        int main() {
            int i = 3;
            auto ptr = std::make_shared<int>(5);
        
            std::cout << dereference(i) << ", " << dereference(ptr) << std::endl;
        
            return 0;
        }
        

        这将打印3, 5

        但是恕我直言,使用回调会更干净。

        template<class T>
        class container {
        private:
            std::vector<T> elements_;
            std::function<void(const T&)> callback_;
        
        public:
            template<typename callback_t>
            container(callback_t callback) {
                callback_ = callback;
            }
        
            void add(T element){
                elements_.push_back(std::move(element));
            }
        
            void process() {
                for(auto& a: elements_){
                    callback_(a);
                }
            }
        };
        

        现在你可以在构造函数中传递回调了:

        container<int> c([](const int& val) {
            std::cout << val << std::endl;
        });
        
        c.add(3);
        c.add(56);
        c.add(4);
        
        c.process();
        

        请记住,您需要包含 functional 标头才能使用 std::function

        【讨论】:

        • 这不是一个糟糕的答案,Alex Stepanov 自己曾经说过,不是指针的类型应该引用自己。实现这种一致性的一种方法是这样的。
        【解决方案6】:

        在这种情况下,我能想到的避免重复代码的最简洁的解决方案是允许用户提供一个可选的函数对象来进行打印。例如:

        template <typename T>
        struct default_print {
          void operator()(T& t) {
            t.print();
          }
        };
        
        template <typename T, typename Printer = default_print<T>>
        class container {
        public:
          container() = default;
        
          container(Printer p) : printer(p) {
          }
        
          void add(T const& element) {
            elements.push_back(element);   
          }
        
          void process() {
            for (auto& e : elements) {
              printer(e);   
            }
          }
        
        private:
          Printer printer;
          std::vector<T> elements;
        };
        

        这很像std::unique_ptr 允许指定自定义删除器。如果需要,您可以使用 empty base class optimization 来获得零大小的无状态打印机开销。

        你会像这样使用container

        struct printable {
          void print() {}
        };
        
        template <typename T>
        struct indirect_print {
          void operator()(T& t) {
            t->print();
          }
        };
        
        int main() {
          container<printable> c1;
          c1.process();
        
          container<printable*, indirect_print<printable*>> c2;
          c2.process();
        }
        

        如果您不喜欢打字,您可以使用一些SFINAE 来实现一个实用功能,如果T 具有-&gt; 运算符,则自动使用不同的打印机:

        template <typename>
        using void_t = void;
        
        template <typename T, typename = void>
        struct has_arrow_operator : std::false_type {
        };
        
        template <typename T>
        struct has_arrow_operator<T, void_t<
            decltype(std::declval<T>().operator->())>> : std::true_type {
        };
        
        template <typename T>
        struct has_arrow : std::integral_constant<bool,
            std::is_pointer<T>::value || has_arrow_operator<T>::value> {
        };
        
        template <typename T, typename = std::enable_if_t<!has_arrow<T>::value>>
        container<T> make_container() {
          return container<T>();
        }
        
        template <typename T, typename = std::enable_if_t<has_arrow<T>::value>>
        container<T, indirect_print<T>> make_container() {
          return container<T, indirect_print<T>>();
        }
        

        你会像这样使用make_container

        int main() {
          auto c1 = make_container<printable>();
          c1.process();
        
          auto c2 = make_container<printable*>();
          c2.process();
        }
        

        您始终可以使用 SFINAE 直接在 container 类中执行打印机切换,但我认为保持该类尽可能通用,并将您的用例封装在实用程序函数中是一种更简洁的设计。

        【讨论】:

        • 请原谅我的无知,但是约瑟夫,如果你能帮助我理解你的答案中的这种语法,我将不胜感激(我的 C++ 知识还过得去,但不是我最强的)decltype(std::declval&lt;T&gt;().operator-&gt;())&gt;&gt; : std::true_type,我知道你是使用 SFINAE 来确定接收器是否是指针,我可以看到你在做什么,但不明白贴出的 decltype 语法。提前致谢。
        • @JakeHeidt 这是表达式 SFINAE。 decltype 用于将表达式转换为类型,以便它可以构成模板参数的一部分。如果表达式无效(在这种情况下,如果T 没有实现operator-&gt;()),那么替换将失败并且不会使用特化。需要void_t 才能进行替换,就像enable_if_t
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-15
        • 1970-01-01
        • 2023-03-15
        • 2019-05-20
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多