首页 文章

C 11中的依赖注入没有原始指针

提问于
浏览
10

我经常在我的项目中使用“依赖注入”模式 . 在C中,最简单的方法是通过传递原始指针来实现,但现在使用C 11,高级代码中的所有内容都应该可以使用智能指针 . 但这个案例的最佳做法是什么?性能并不重要,干净且易懂的代码对我来说更重要 .

让我举一个简化的例子 . 我们有一个使用里面距离计算的算法 . 我们希望能够用不同的距离度量(Euclidean,Manhattan等)替换此计算 . 我们的目标是能够说出类似的话:

SomeAlgorithm algorithmWithEuclidean(new EuclideanDistanceCalculator());
SomeAlgorithm algorithmWithManhattan(new ManhattanDistanceCalculator());

但有智能指针,以避免手动 newdelete . 这是一个使用原始指针的可能实现:

class DistanceCalculator {
public:
    virtual double distance(Point p1, Point p2) = 0;
};

class EuclideanDistanceCalculator {
public:
    virtual double distance(Point p1, Point p2) {
        return sqrt(...);
    }
};

class ManhattanDistanceCalculator {
public:
    virtual double distance(Point p1, Point p2) {
        return ...;
    }
};

class SomeAlgorithm {
    DistanceCalculator* distanceCalculator;

public:
    SomeAlgorithm(DistanceCalculator* distanceCalculator_)
        : distanceCalculator(distanceCalculator_) {}

    double calculateComplicated() {
        ...
        double dist = distanceCalculator->distance(p1, p2);
        ...
    }

    ~SomeAlgorithm(){
        delete distanceCalculator;
    }
};

让's assume that copying is not really an issue, and if we didn' t需要多态,我们只需将 DistanceCalculator 传递给 SomeAlgorithm 的构造函数(复制) . 但由于我们需要能够传入不同的派生实例(不进行切片),因此参数必须是原始指针,引用或智能指针 .

我想到的一个解决方案是通过reference-to-const传递它并将其封装在 std::unique_ptr<DistanceCalculator> 成员变量中 . 那么电话会是:

SomeAlgorithm algorithmWithEuclidean(EuclideanDistance());

但是这个堆栈分配的临时对象(rvalue-reference?)将在此行之后被破坏 . 因此,我们需要进行一些复制,使其更像是传值 . 但由于我们不知道运行时类型,因此我们无法轻松构建我们的副本 .

我们还可以使用智能指针作为构造函数参数 . 由于所有权没有问题( DistanceCalculator 将归 SomeAlgorithm 所有),我们应该使用 std::unique_ptr . 我真的应该用 unique_ptr 替换所有这样的构造函数参数吗?它似乎降低了可读性 . 此外, SomeAlgorithm 的用户必须以一种尴尬的方式构建它:

SomeAlgorithm algorithmWithEuclidean(std::unique_ptr<DistanceCalculator>(new EuclideanDistance()));

或者我应该以某种方式使用新的移动语义(&&,std :: move)?

这似乎是一个非常标准的问题,必须有一些简洁的方法来实现它 .

3 回答

  • 9

    如果我想这样做,我要做的第一件事是杀死你的界面,而是使用它:

    SomeAlgorithm(std::function<double(Point,Point)> distanceCalculator_)
    

    类型擦除调用对象 .

    我可以像你这样使用你的_2560869进行直接替换:

    std::function<double(Point,Point)> UseEuclidean() {
      auto obj = std::make_shared<EuclideanDistance>();
      return [obj](Point a, Point b)->double {
        return obj->distance( a, b );
      };
    }
    SomeAlgorithm foo( UseEuclidean() );
    

    但由于距离计算器很少需要状态,我们可以取消对象 .

    在C 1y的支持下,这缩短为:

    std::function<double(Point,Point>> UseEuclidean() {
      return [obj = std::make_shared<EuclideanDistance>()](Point a, Point b)->double {
        return obj->distance( a, b );
      };
    }
    

    因为它不再需要局部变量,可以内联使用:

    SomeAlgorithm foo( [obj = std::make_shared<EuclideanDistance>()](Point a, Point b)->double {
        return obj->distance( a, b );
      } );
    

    但同样, EuclideanDistance 没有任何真实的状态,所以相反我们可以

    std::function<double(Point,Point>> EuclideanDistance() {
      return [](Point a, Point b)->double {
        return sqrt( (b.x-a.x)*(b.x-a.x) + (b.y-a.y)*(b.y*a.y) );
      };
    }
    

    如果我们确实不需要移动但我们确实需要状态,我们可以编写一个不支持基于非移动的赋值的 unique_function< R(Args...) > 类型,并存储其中一个 .

    其核心是接口 DistanceCalculator 是噪声 . 变量的名称通常就足够了 . std::function< double(Point,Point) > m_DistanceCalculator 明确了它的作用 . 类型擦除对象 std::function 的创建者处理任何生命周期管理问题,我们只按值存储 function 对象 .

    如果你的实际依赖注入更复杂(比如多个不同的相关回调),使用接口也不错 . 如果你想避免复制要求,我会这样做:

    struct InterfaceForDependencyStuff {
      virtual void method1() = 0;
      virtual void method2() = 0;
      virtual int method3( double, char ) = 0;
      virtual ~InterfaceForDependencyStuff() {}; // optional if you want to do more work later, but probably worth it
    };
    

    然后,写下你自己的 make_unique<T>(Args&&...) (一个 std 将在C 1y中出现),并像这样使用它:

    接口:

    SomeAlgorithm(std::unique_ptr<InterfaceForDependencyStuff> pDependencyStuff)
    

    使用:

    SomeAlgorithm foo(std::make_unique<ImplementationForDependencyStuff>( blah blah blah ));
    

    如果您不想 virtual ~InterfaceForDependencyStuff() 并且想要使用 unique_ptr ,则必须使用存储其删除器的 unique_ptr (通过传入有状态删除器) .

    另一方面,如果 std::shared_ptr 已经附带 make_sharedand 默认情况下会将其删除状态存储起来 . 因此,如果你使用界面的 shared_ptr 存储,你会得到:

    SomeAlgorithm(std::shared_ptr<InterfaceForDependencyStuff> pDependencyStuff)
    

    SomeAlgorithm foo(std::make_shared<ImplementationForDependencyStuff>( blah blah blah ));
    

    make_shared 将存储一个删除 ImplementationForDependencyStuff 的函数指针,该函数在将其转换为 std::shared_ptr<InterfaceForDependencyStuff> 时不会丢失,因此您可以在 InterfaceForDependencyStuff 中安全地缺少 virtual 析构函数 . 我个人不会打扰,并留下 virtual ~InterfaceForDependencyStuff .

  • 1

    在大多数情况下,您不需要或不需要所有权转移,这会使代码更难理解,灵活性也会降低(移动对象无法重复使用) . 典型的情况是保持呼叫者的所有权:

    class SomeAlgorithm {
        DistanceCalculator* distanceCalculator;
    
    public:
        explicit SomeAlgorithm(DistanceCalculator* distanceCalculator_)
            : distanceCalculator(distanceCalculator_) {
            if (distanceCalculator == nullptr) { abort(); }
        }
    
        double calculateComplicated() {
            ...
            double dist = distanceCalculator->distance(p1, p2);
            ...
        }
    
        // Default special members are fine.
    };
    
    int main() {
        EuclideanDistanceCalculator distanceCalculator;
        SomeAlgorithm algorithm(&distanceCalculator);
        algorithm.calculateComplicated();
    }
    

    原始指针可以表达非所有权 . 如果您愿意,可以在构造函数参数中使用引用,它没有任何实际区别 . 但是,不要使用引用作为数据成员,它会使类不必要地无法分配 .

  • 3

    只使用任何指针的缺点(智能或原始),甚至是普通的C引用,它们允许从const上下文中调用非const方法 .

    对于没有问题的单个方法的无状态类, std::function 是一个很好的选择,但是对于具有状态或多个方法的类的一般情况,我提出了一个与 std::reference_wrapper (缺少const安全访问器)相似但不相同的包装器) .

    template<typename T>
      struct NonOwningRef{
        NonOwningRef() = delete;
        NonOwningRef(T& other) noexcept : ptr(std::addressof(other)) { };
        NonOwningRef(const NonOwningRef& other) noexcept = default;
    
        const T& value() const noexcept{ return *ptr; };
        T& value() noexcept{ return *ptr; };
    
      private:
        T* ptr;
      };
    

    用法:

    class SomeAlgorithm {
          NonOwningRef<DistanceCalculator> distanceCalculator;
    
       public:
          SomeAlgorithm(DistanceCalculator& distanceCalculator_)
            : distanceCalculator(distanceCalculator_) {}
    
          double calculateComplicated() {
    
             double dist = distanceCalculator.value().distance(p1, p2);
             return dist;
        }
    };
    

    T* 替换为 unique_ptrshared_ptr 以获取拥有版本 . 在这种情况下,还可以添加任何 unique_ptr<T2>shared_ptr<T2> 的移动构造和构造 .

相关问题