C++ 泛型 class 处理解引用语法

C++ generic class dealing with dereferencing syntax

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

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

来自 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();
}

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

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

现在您可以向它传递任何 object、智能指针(已弃用的 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 header 才能使用 std::function

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

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 allows a custom deleter to be specified. You could use the 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-> 运算符,它会自动使用不同的打印机:

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 class 中执行打印机切换,但我觉得保持 class 尽可能通用,并封装您的用例在效用函数中是一个更简洁的设计。

作为另一个答案中建议的使用打印机类型参数化 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(); });

布莱尔,

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

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

特征 class、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++ 中并不经常出现,因为我们通常不想以相同的方式处理对象和指向它们的东西。也就是说,我希望这展示了一种可用于在特定情况下实现该目标的方法。

What is the best way to deal with the fact that some types require members / methods to be accessed with the . operator whilst others with the -> operator.

只是不要。

你的工作是写作template<class T> class container。该容器包含 Ts。如果您的用户想在 T 上做某事,您应该公开做某事的能力 - 但 他们有责任 正确执行该操作。否则,您只是在添加大量代码。太好了,你给了我一种打印所有元素的方法,但是如果我知道如何调用它们 foo() ,或者找到第一个 bar() returns 大于 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);

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

Important is_pointer detects "real" pointer types only, and not smart pointers. Users should not specialise is_pointer for smart pointer types, as doing so may cause Boost (and other third party) code to fail to function correctly. Users wanting a trait to detect smart pointers should create their own. However, note that there is no way in general to auto-magically detect smart pointer types, so such a trait would have to be partially specialised for each supported smart pointer type.

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