有没有办法在不使用new的情况下从函数返回抽象(出于性能原因)

时间:2016-07-11 11:01:35

标签: c++ oop

例如,我有一些函数pet_maker()可以创建并返回CatDog作为基础Pet。我想多次调用此函数,并对返回的Pet执行某些操作。

传统上我会new CatDog pet_maker()并返回指向它的指针,但是new调用比执行所有操作要慢得多在堆栈上。

有没有人能够想到的一种简洁的方式,作为一个抽象返回,而不必每次调用函数时都做新的,或者是否有其他方法可以快速创建和返回抽象?

9 个答案:

答案 0 :(得分:20)

如果你想要多态,那么使用new几乎是不可避免的。但新工作缓慢的原因是因为它每次都在寻找免费记忆。您可以做的是编写自己的运算符new,理论上可以使用预先分配的内存块并且速度非常快。

This article涵盖了您可能需要的许多方面。

答案 1 :(得分:10)

每个分配都是一个开销,因此您可以通过分配整个对象数组而不是一次分配一个对象来获益。

您可以使用std::deque来实现此目标:

class Pet { public: virtual ~Pet() {} virtual std::string talk() const = 0; };
class Cat: public Pet { std::string talk() const override { return "meow"; }};
class Dog: public Pet { std::string talk() const override { return "woof"; }};
class Pig: public Pet { std::string talk() const override { return "oink"; }};

class PetMaker
{
    // std::deque never re-allocates when adding
    // elements which is important when distributing
    // pointers to the elements
    std::deque<Cat> cats;
    std::deque<Dog> dogs;
    std::deque<Pig> pigs;

public:

    Pet* make()
    {
        switch(std::rand() % 3)
        {
            case 0:
                cats.emplace_back();
                return &cats.back();
            case 1:
                dogs.emplace_back();
                return &dogs.back();
        }
        pigs.emplace_back();
        return &pigs.back();
    }
};

int main()
{
    std::srand(std::time(0));

    PetMaker maker;

    std::vector<Pet*> pets;

    for(auto i = 0; i < 100; ++i)
        pets.push_back(maker.make());

    for(auto pet: pets)
        std::cout << pet->talk() << '\n';
}

使用std::deque的原因是,当您添加新元素时永远不会重新分配其元素,因此您分发的指针始终有效,直到PetMaker本身为止删除。

单独分配对象的另一个好处是,它们不需要删除或放在智能指针中,std::deque可以管理它们的生命周期。

答案 2 :(得分:10)

  

每次调用函数时,是否有人能够想到以一种抽象方式返回而不必执行new,或者是否有其他方法可以快速创建和返回抽象? / p>

TL; DR:如果已有足够的内存可供使用,则无需分配该功能。

一种简单的方法是创建一个与其兄弟姐妹略有不同的智能指针:它将包含一个缓冲区,用于存储该对象。我们甚至可以让它不可空!

长版:

我将以相反的顺序呈现草稿,从动机到棘手的细节:

class Pet {
public:
    virtual ~Pet() {}

    virtual void say() = 0;
};

class Cat: public Pet {
public:
    virtual void say() override { std::cout << "Miaou\n"; }
};

class Dog: public Pet {
public:
    virtual void say() override { std::cout << "Woof\n"; }
};

template <>
struct polymorphic_value_memory<Pet> {
    static size_t const capacity = sizeof(Dog);
    static size_t const alignment = alignof(Dog);
};

typedef polymorphic_value<Pet> any_pet;

any_pet pet_factory(std::string const& name) {
    if (name == "Cat") { return any_pet::build<Cat>(); }
    if (name == "Dog") { return any_pet::build<Dog>(); }

    throw std::runtime_error("Unknown pet name");
}

int main() {
    any_pet pet = pet_factory("Cat");
    pet->say();
    pet = pet_factory("Dog");
    pet->say();
    pet = pet_factory("Cat");
    pet->say();
}

预期产出:

Miaou
Woof
Miaou

you can find here

请注意,需要指定可支持的派生值的最大大小和对齐方式。没办法。

当然,我们会静态检查调用者是否会尝试使用不合适的类型来构建一个值,以避免任何不愉快。

当然,主要的缺点是它必须至少与其最大的变体一样大(并且对齐),所有这一切必须提前预测。因此,这不是一个灵丹妙药,但在性能方面,缺乏记忆分配可能会撼动。

它是如何工作的?使用这个高级类(和帮助器):

//  To be specialized for each base class:
//  - provide capacity member (size_t)
//  - provide alignment member (size_t)
template <typename> struct polymorphic_value_memory;

template <typename T,
          typename CA = CopyAssignableTag,
          typename CC = CopyConstructibleTag,
          typename MA = MoveAssignableTag,
          typename MC = MoveConstructibleTag>
class polymorphic_value {
    static size_t const capacity = polymorphic_value_memory<T>::capacity;
    static size_t const alignment = polymorphic_value_memory<T>::alignment;

    static bool const move_constructible = std::is_same<MC, MoveConstructibleTag>::value;
    static bool const move_assignable = std::is_same<MA, MoveAssignableTag>::value;
    static bool const copy_constructible = std::is_same<CC, CopyConstructibleTag>::value;
    static bool const copy_assignable = std::is_same<CA, CopyAssignableTag>::value;

    typedef typename std::aligned_storage<capacity, alignment>::type storage_type;

public:
    template <typename U, typename... Args>
    static polymorphic_value build(Args&&... args) {
        static_assert(
            sizeof(U) <= capacity,
            "Cannot host such a large type."
        );

        static_assert(
            alignof(U) <= alignment,
            "Cannot host such a largely aligned type."
        );

        polymorphic_value result{NoneTag{}};
        result.m_vtable = &build_vtable<T, U, MC, CC, MA, CA>();
        new (result.get_ptr()) U(std::forward<Args>(args)...);
        return result;
    }

    polymorphic_value(polymorphic_value&& other): m_vtable(other.m_vtable), m_storage() {
        static_assert(
            move_constructible,
            "Cannot move construct this value."
        );

        (*m_vtable->move_construct)(&other.m_storage, &m_storage);

        m_vtable = other.m_vtable;
    }

    polymorphic_value& operator=(polymorphic_value&& other) {
        static_assert(
            move_assignable || move_constructible,
            "Cannot move assign this value."
        );

        if (move_assignable && m_vtable == other.m_vtable)
        {
            (*m_vtable->move_assign)(&other.m_storage, &m_storage);
        }
        else
        {
            (*m_vtable->destroy)(&m_storage);

            m_vtable = other.m_vtable;
            (*m_vtable->move_construct)(&other.m_storage, &m_storage);
        }

        return *this;
    }

    polymorphic_value(polymorphic_value const& other): m_vtable(other.m_vtable), m_storage() {
        static_assert(
            copy_constructible,
            "Cannot copy construct this value."
        );

        (*m_vtable->copy_construct)(&other.m_storage, &m_storage);
    }

    polymorphic_value& operator=(polymorphic_value const& other) {
        static_assert(
            copy_assignable || (copy_constructible && move_constructible),
            "Cannot copy assign this value."
        );

        if (copy_assignable && m_vtable == other.m_vtable)
        {
            (*m_vtable->copy_assign)(&other.m_storage, &m_storage);
            return *this;
        }

        //  Exception safety
        storage_type tmp;
        (*other.m_vtable->copy_construct)(&other.m_storage, &tmp);

        if (move_assignable && m_vtable == other.m_vtable)
        {
            (*m_vtable->move_assign)(&tmp, &m_storage);
        }
        else
        {
            (*m_vtable->destroy)(&m_storage);

            m_vtable = other.m_vtable;
            (*m_vtable->move_construct)(&tmp, &m_storage);
        }

        return *this;
    }

    ~polymorphic_value() { (*m_vtable->destroy)(&m_storage); }

    T& get() { return *this->get_ptr(); }
    T const& get() const { return *this->get_ptr(); }

    T* operator->() { return this->get_ptr(); }
    T const* operator->() const { return this->get_ptr(); }

    T& operator*() { return this->get(); }
    T const& operator*() const { return this->get(); }

private:
    polymorphic_value(NoneTag): m_vtable(0), m_storage() {}

    T* get_ptr() { return reinterpret_cast<T*>(&m_storage); }
    T const* get_ptr() const { return reinterpret_cast<T const*>(&m_storage); }

    polymorphic_value_vtable const* m_vtable;
    storage_type m_storage;
}; // class polymorphic_value

基本上,这就像任何STL容器一样。大部分复杂性在于重新定义构造,移动,复制和销毁。否则很简单。

有两点值得注意:

  1. 我使用基于标签的方法来处理功能:

    • 例如,复制构造函数仅在传递CopyConstructibleTag
    • 时可用
    • 如果CopyConstructibleTag通过,则传递给build 的所有类型必须可复制构建
  2. 即使对象没有该功能,也会提供一些操作,只要提供一些替代方法

  3. 显然,所有方法都保留了polymorphic_value永远不会为空的不变量。

    还有一个与分配相关的棘手细节:如果两个对象具有相同的动态类型,则只能很好地定义赋值,我们会使用m_vtable == other.m_vtable检查来检查。

    为了完整起见,用于启动此课程的缺失部分:

    //
    //  VTable, with nullable methods for run-time detection of capabilities
    //
    struct NoneTag {};
    struct MoveConstructibleTag {};
    struct CopyConstructibleTag {};
    struct MoveAssignableTag {};
    struct CopyAssignableTag {};
    
    struct polymorphic_value_vtable {
        typedef void (*move_construct_type)(void* src, void* dst);
        typedef void (*copy_construct_type)(void const* src, void* dst);
        typedef void (*move_assign_type)(void* src, void* dst);
        typedef void (*copy_assign_type)(void const* src, void* dst);
        typedef void (*destroy_type)(void* dst);
    
        move_construct_type move_construct;
        copy_construct_type copy_construct;
        move_assign_type move_assign;
        copy_assign_type copy_assign;
        destroy_type destroy;
    };
    
    
    template <typename Base, typename Derived>
    void core_move_construct_function(void* src, void* dst) {
        Derived* derived = reinterpret_cast<Derived*>(src);
        new (reinterpret_cast<Base*>(dst)) Derived(std::move(*derived));
    } // core_move_construct_function
    
    template <typename Base, typename Derived>
    void core_copy_construct_function(void const* src, void* dst) {
        Derived const* derived = reinterpret_cast<Derived const*>(src);
        new (reinterpret_cast<Base*>(dst)) Derived(*derived);
    } // core_copy_construct_function
    
    template <typename Derived>
    void core_move_assign_function(void* src, void* dst) {
        Derived* source = reinterpret_cast<Derived*>(src);
        Derived* destination = reinterpret_cast<Derived*>(dst);
        *destination = std::move(*source);
    } // core_move_assign_function
    
    template <typename Derived>
    void core_copy_assign_function(void const* src, void* dst) {
        Derived const* source = reinterpret_cast<Derived const*>(src);
        Derived* destination = reinterpret_cast<Derived*>(dst);
        *destination = *source;
    } // core_copy_assign_function
    
    template <typename Derived>
    void core_destroy_function(void* dst) {
        Derived* d = reinterpret_cast<Derived*>(dst);
        d->~Derived();
    } // core_destroy_function
    
    
    template <typename Tag, typename Base, typename Derived>
    typename std::enable_if<
        std::is_same<Tag, MoveConstructibleTag>::value,
        polymorphic_value_vtable::move_construct_type
    >::type 
    build_move_construct_function()
    {
        return &core_move_construct_function<Base, Derived>;
    } // build_move_construct_function
    
    template <typename Tag, typename Base, typename Derived>
    typename std::enable_if<
        std::is_same<Tag, CopyConstructibleTag>::value,
        polymorphic_value_vtable::copy_construct_type
    >::type 
    build_copy_construct_function()
    {
        return &core_copy_construct_function<Base, Derived>;
    } // build_copy_construct_function
    
    template <typename Tag, typename Derived>
    typename std::enable_if<
        std::is_same<Tag, MoveAssignableTag>::value,
        polymorphic_value_vtable::move_assign_type
    >::type 
    build_move_assign_function()
    {
        return &core_move_assign_function<Derived>;
    } // build_move_assign_function
    
    template <typename Tag, typename Derived>
    typename std::enable_if<
        std::is_same<Tag, CopyAssignableTag>::value,
        polymorphic_value_vtable::copy_construct_type
    >::type 
    build_copy_assign_function()
    {
        return &core_copy_assign_function<Derived>;
    } // build_copy_assign_function
    
    
    template <typename Base, typename Derived,
              typename MC, typename CC,
              typename MA, typename CA>
    polymorphic_value_vtable const& build_vtable() {
        static polymorphic_value_vtable const V = {
            build_move_construct_function<MC, Base, Derived>(),
            build_copy_construct_function<CC, Base, Derived>(),
            build_move_assign_function<MA, Derived>(),
            build_copy_assign_function<CA, Derived>(),
            &core_destroy_function<Derived>
        };
        return V;
    } // build_vtable
    

    我在这里使用的一个技巧是让用户配置他将在这个容器中使用的类型是否可以移动构造,移动分配,......通过能力标签。许多操作都锁定在这些标签上,如果请求的功能

    ,将被禁用或效率降低

答案 3 :(得分:6)

您可以创建堆栈分配器实例(当然有一些最大限制)并将其作为参数传递给pet_maker函数。然后代替常规new在堆栈分配器提供的地址上执行placement new

在超过堆栈分配器的new时,您也可以默认为max_size

答案 4 :(得分:4)

一种方法是通过分析预先计算出你的程序需要多少种类的对象。

然后您可以提前分配适当大小的数组,只要您有记账来跟踪分配。

例如;

#include <array>

//   Ncats, Ndogs, etc are predefined constants specifying the number of cats and dogs

std::array<Cat, Ncats> cats;
std::array<Dog, Ndogs> dogs;

//  bookkeeping - track the returned number of cats and dogs

std::size_t Rcats = 0, Rdogs = 0;

Pet *pet_maker()
{
    // determine what needs to be returned

    if (return_cat)
    {
       assert(Rcats < Ncats);
       return &cats[Rcats++];
    }
    else if (return_dog)
    {
       assert(Rdogs < Ndogs);
       return &dogs[Rdogs++];
    }
    else
    {
        // handle other case somehow
    }
}

当然,最大的权衡是要求事先明确确定每种动物的数量 - 并分别跟踪每种类型。

但是,如果您希望避免动态内存分配(运算符new),那么这种方式 - 尽管看似严苛 - 提供了绝对的保证。使用运算符new显式允许在运行时确定所需的对象数。相反,为避免使用运算符new但允许某些函数安全地访问许多对象,必须预先确定对象的数量。

答案 5 :(得分:3)

这取决于您拥有的确切用例,以及您愿意容忍的限制。例如,如果您可以重复使用相同的对象而不是每次都有新的副本,则可以返回对函数内部静态对象的引用:

Pet& pet_maker()
{
static Dog dog;
static Cat cat;

    //...

    if(shouldReturnDog) {
        //manipulate dog as necessary
        //...
        return dog;
    }
    else
    {
        //manipulate cat as necessary
        //...
        return cat;
    }
}

如果客户端代码接受它不拥有返回的对象并且重用相同的物理实例,则此方法有效。

如果这组特定的假设不合适,还有其​​他技巧可以使用。

答案 6 :(得分:3)

在某些时候,某人将不得不分配内存并初始化对象。如果按需执行它们,通过sp_executesql使用堆的时间太长,那么为什么不在池中预先分配一些数量。然后,您可以根据需要初始化每个单独的对象。缺点是你可能会在一段时间内放置一堆额外的物体。

如果实际初始化对象是问题,而不是内存分配,那么您可以考虑保留预先构建的对象并使用Pototype模式来更快地初始化。

为获得最佳效果,内存分配是问题和初始化时间,您可以将这两种策略结合起来。

答案 7 :(得分:3)

您可能需要考虑使用(Boost)变体。调用者需要额外的步骤,但它可能符合您的需求:

#include <boost/variant/variant.hpp>
#include <boost/variant/get.hpp>
#include <iostream>

using boost::variant;
using std::cout;


struct Pet {
    virtual void print_type() const = 0;
};

struct Cat : Pet {
    virtual void print_type() const { cout << "Cat\n"; }
};

struct Dog : Pet {
    virtual void print_type() const { cout << "Dog\n"; }
};


using PetVariant = variant<Cat,Dog>;
enum class PetType { cat, dog };


PetVariant make_pet(PetType type)
{
    switch (type) {
        case PetType::cat: return Cat();
        case PetType::dog: return Dog();
    }

    return {};
}

Pet& get_pet(PetVariant& pet_variant)
{
    return apply_visitor([](Pet& pet) -> Pet& { return pet; },pet_variant);
}




int main()
{
    PetVariant pet_variant_1 = make_pet(PetType::cat);
    PetVariant pet_variant_2 = make_pet(PetType::dog);
    Pet& pet1 = get_pet(pet_variant_1);
    Pet& pet2 = get_pet(pet_variant_2);
    pet1.print_type();
    pet2.print_type();
}

输出:

Cat
Dog

答案 8 :(得分:1)

  

例如,我有一些函数pet_maker()可以创建并返回CatDog作为基础Pet。我想多次调用此函数,并对返回的Pet执行某些操作。

如果您在完成宠物之后立即丢弃宠物,您可以使用以下示例中显示的技术:

#include<iostream>
#include<utility>

struct Pet {
    virtual ~Pet() = default;
    virtual void foo() const = 0;
};

struct Cat: Pet {
    void foo() const override {
        std::cout << "cat" << std::endl;
    }
};

struct Dog: Pet {
    void foo() const override {
        std::cout << "dog" << std::endl;
    }
};

template<typename T, typename F>
void factory(F &&f) {
    std::forward<F>(f)(T{});
}

int main() {
    auto lambda = [](const Pet &pet) { pet.foo(); };
    factory<Cat>(lambda);
    factory<Dog>(lambda);
}

根本不需要分配。基本思想是恢复逻辑:工厂不再返回对象。相反,它调用一个函数提供正确的实例作为参考 如果您想将对象复制并存储在某处,则会出现此方法的问题 因为从问题中不清楚,值得提出这个解决方案。