Использование лямбда для создания произвольной оболочки класса вокруг некоторого объекта типа T

Экспериментируя с некоторыми функциями языка C ++, я смог успешно спроектировать вызываемый вызов безымянной функции с помощью лямбда-выражения, который обычно создает произвольную оболочку класса, которая возвращает объект класса, без необходимости иметь прямую реализацию этого класса внутри своего собственная отдельная переводческая единица. Сам класс-оболочка по-прежнему содержится в единице перевода, поскольку он объявлен и определен в рамках лямбда-выражения. Затем эта лямбда возвращает этот тип с помощью вывода типа шаблона.

В моем случае я создал класс Wrapper вокруг типа T объект тем, что он хранит указатель на этот объект, а его конструктор и деструктор соответственно вызывают new и delete для этого указателя. Члены являются частными и доступны только через открытый интерфейс класса, и они объявлены как const немодифицируемый означает, что использование вызова этих методов не изменит состояние создаваемого объекта класса. Чтобы лучше проиллюстрировать это, вот как выглядит мой исходный код:

some.h

#include <string>
#include <string_view>

template<typename T>
auto create_dynamic_wrapper = [&](const std::string_view name, T value) {
    class DynamicWrapper {
    public:
        DynamicWrapper() = default;

        DynamicWrapper(const std::string_view name_in, T value) : name_{ name_in } {
            this->pData_ = new T{ value };
        }

        ~DynamicWrapper() {
            if (nullptr != this->pData_) {
                delete this->pData_;
                this->pData_ = nullptr;
            }
        }

        // define copy constructor and assignment operator here
        // wrt how you want this wrapper class to behave in order
        // to preserve the rule of 3 or 5. Meaning, do you want
        // to allow this class object to be copyable or not... 

        auto value() const { return *(this->pData_); }
        auto ptr() const { return this->pData_; }
        auto name() const { return this->name_; }
    private:
        T* pData_ = nullptr;
        const std::string name_;
    };
    return DynamicWrapper(name, value);
}; 

Программа вождения

основной

#include <iostream>

#include "some.h"

int main() {
    auto C = create_dynamic_wrapper<int>("Foo", 7);
    std::cout << C.name() << 'n';
    std::cout << C.ptr() << 'n';
    std::cout << C.value() << 'n';
    std::cout << typeid(C).name() << 'n';
    return 0;
}

Когда я запускаю это на своей машине, я получаю следующий результат:

Вывод

Foo
00000000006857F0
7
class `public: __cdecl <lambda_986b701ab3be1e8cd3d0d2d875c96c7d>::operator()(class std::basic_string_view<char,struct st
d::char_traits<char> >,int)const __ptr64'::`2'::DynamicWrapper

Вторая строка будет отличаться, поскольку в ней используется динамическое выделение памяти в куче. Четвертая строка будет различаться в зависимости от компилятора, операционной системы и других факторов из-за соглашений об именах, изменения имен, генерации символов и т. Д.

Теперь о моих вопросах и опасениях …


  • Я не знаю, имеет ли этот тип структуры / генерации кода конкретное идиоматическое имя. Если да, то как бы назвать эту идиому?
  • Считается ли это хорошо определенной программой?
  • Каковы последствия использования такой структуры:
    • Будет ли это вызывать какой-либо UB?
    • Может ли это привести к утечкам памяти, недействительным или висячим указателям или ссылкам?
    • Будет ли это считаться безопасным для потоков и исключений?
  • Объясните мне, в чем, по вашему мнению, есть плюсы и минусы использования такого рода генерации кода?
  • Без использования smart-pointers есть ли что-нибудь еще, о чем мне нужно было бы знать, когда дело доходит до использования динамической памяти при использовании new и delete внутри автономного объекта, определенного в этом контексте?
  • Каковы побочные эффекты использования такой структуры?
  • Что я могу или что мне нужно сделать, чтобы улучшить этот фрагмент кода, чтобы сделать его четко определенной базой кода, которая не представляет никакого потенциального UB?
  • Каковы были бы потенциальные возможности использования этого типа реализации?
  • Если все необходимые меры предосторожности будут приняты для устранения запаха кода … будет ли такая структура кода полезным инструментом в любом виде производственного кода?

-Примечание для читателя-

Об этой структуре кода можно сказать что-то интересное. Сама лямбда внутренне и локально объявляет и определяет объект класса с именем DynamicWrapper<T> где этот класс определен и объявлен в рамках этой лямбды и с использованием auto type deduction он возвращает созданный экземпляр класса этого типа.

Затем внутри некоторой единицы перевода, которая вызывает и вызывает эту лямбду, и с помощью auto спецификатор, именованный объект, который возвращается в объявленный пользователем auto variable на самом деле DynamicWrapper<T> экземпляр, даже если такое объявление или определение класса не существует вне этой вызванной лямбды. Я считаю, что у этого есть очень интересный набор свойств и поведения в отношении его общего шаблона проектирования и реализации.

2 ответа
2

Я не знаю, имеет ли этот тип структуры / генерации кода конкретное идиоматическое имя. Если да, то как бы назвать эту идиому?

Я не уверен.

Считается ли это хорошо определенной программой?

Мне нравится.

Каковы последствия использования такой структуры:

  • Будет ли это вызывать какой-либо UB?

Выглядит хорошо сформированным.

  • Может ли это привести к утечкам памяти, недействительным или висячим указателям или ссылкам?

В общем случае нет.
В этом конкретном случае: вы не реализовали правило трех / пяти, и вы управляете динамически создаваемым ресурсом внутри класса. Таким образом, определенно существует вероятность неправильного использования ресурсов.

Будет ли это считаться безопасным для потоков и исключений?

Ничто не является потокобезопасным, если вы явно не сделаете это так (кроме вещей, специально предназначенных для потоковой передачи: atomic, mutex, condition_variable так далее…).

Исключение безопасно. Да, если решено правило трех.

Объясните мне, в чем, по вашему мнению, есть плюсы и минусы использования такого рода генерации кода?

Не уверен, что для этого вам нужна лямбда:
Стандартный шаблон — использовать make_X функция. Увидеть: make_pair().

template<typename T>
class DynamicWrapper {
public:
    DynamicWrapper() = default;

    DynamicWrapper(const std::string_view name_in, T value) : name_{ name_in } {
        this->pData_ = new T{ value };
    }

    ~DynamicWrapper() {
        if (nullptr != this->pData_) {
            delete this->pData_;
            this->pData_ = nullptr;
        }
    }

    auto value() const { return *(this->pData_); }
    auto ptr() const { return this->pData_; }
    auto name() const { return this->name_; }
private:
    T* pData_ = nullptr;
    const std::string name_;
};

template<typename T>
DynamicWrapper<T> make_DynamicWrapper(std::string_view name_in, T&& value)
{
    return DynamicWrapper<T>(std::move(name_in), std::forward<T>(value));
}

int main()
{
    auto C = make_DynamicWrapper("Foo", 7);
}

Без использования интеллектуальных указателей есть ли что-нибудь еще, о чем мне нужно было бы знать, когда дело доходит до использования динамической памяти при использовании новых и удаленных внутри автономного объекта, определенного в этом контексте?

Соблюдайте правило трех / пяти.

Каковы побочные эффекты использования такой структуры?

Не важно.

Что я могу или что мне нужно сделать, чтобы улучшить этот фрагмент кода, чтобы сделать его четко определенной базой кода, которая не представляет никакого потенциального UB?

Выглядит неплохо.

Каковы были бы потенциальные возможности использования этого типа реализации?

Не уверен, что это значит.

Если приняты все необходимые меры для устранения запаха кода …

Никаких запахов.

Будет ли такая структура кода полезным инструментом в любом виде производственного кода?

Уверенный.


Следует отметить, что лямбда-выражения — это просто сокращенная нотация для написания функторов. Итак, мы могли бы переписать вашу лямбду следующим образом (что показывает, что на самом деле происходит в компиляторе).

template<typename T>
struct Lambda_986b701ab3be1e8cd3d0d2d875c96c7d
{
    class DynamicWrapper {
    public:
        DynamicWrapper() = default;

        DynamicWrapper(const std::string_view name_in, T value) : name_{ name_in } {
            this->pData_ = new T{ value };
        }

        ~DynamicWrapper() {
            if (nullptr != this->pData_) {
                delete this->pData_;
                this->pData_ = nullptr;
            }
        }

        // define copy constructor and assignment operator here
        // wrt how you want this wrapper class to behave in order
        // to preserve the rule of 3 or 5. Meaning, do you want
        // to allow this class object to be copyable or not... 

        auto value() const { return *(this->pData_); }
        auto ptr() const { return this->pData_; }
        auto name() const { return this->name_; }
    private:
        T* pData_ = nullptr;
        const std::string name_;
    };

    DynamicWrapper operator()(const std::string_view name, T value) const
    {
        return DynamicWrapper(name, value);
    }
};

int main()
{
    auto c = Lambda_986b701ab3be1e8cd3d0d2d875c96c7d<int>{}("Fun", 7);
}

  • Насчет правила 3 ​​или 5 … Я не включил copy constructor ни assignment operator в том смысле, что они должны быть «понятными без объяснений» или хорошо понимаемыми, и в мыслях должно быть ощущение, что они в каком-то смысле не имеют отношения к реальной реализации его дизайна. В остальном ваш ответ очень простой, лаконичный и по существу!

    — Фрэнсис Куглер

  • Теперь, поскольку указанное выше поколение класса обрабатывает распределение своей памяти внутри и тот факт, что он не может быть изменен извне своего класса, можно ли с уверенностью сказать, что конструктор копирования и оператор присваивания по умолчанию будут жизнеспособными в этом контексте? Я не решил, должен ли сам сгенерированный объект-оболочка копироваться или нет … это еще одна причина их прямого упущения. Думаю, я мог бы закомментировать их описанием, объясняющим их упущения.

    — Фрэнсис Куглер

  • Я также знаю, что мог бы сделать это с помощью class template и function. Однако я объединил их в одно лямбда-выражение, просто чтобы изучить некоторые особенности и свойства лямбда-выражений в современном C ++.

    — Фрэнсис Куглер


  • Я добавил к классу комментарий относительно его конструктора копирования и оператора присваивания, чтобы отразить ваше утверждение о правиле 3 или 5, чтобы будущие читатели этого Q / A могли видеть причину своего явного упущения. Идея оставлять комментарии заключается в том, чтобы заявить, что они должны быть определены там, но что они определены реализацией в отношении предполагаемого использования этого класса разработчиком, что делает класс копируемым или не копируемым.

    — Фрэнсис Куглер

  • «Make» функции вроде как идут по пути додо в C ++ 17; make_pair() уже устарело. Я бы сказал, забудьте про лямбду и функцию make и просто используйте вывод параметров шаблона (при необходимости с соответствующим руководством по выводам): auto c = DynamicWrapper{"foo", 7};.

    — инди

Во-первых, совсем непонятно, для чего это, чем не отличается std::pair<T, std::string>. Какая мотивация?


Это не будет работать в C ++ 20:

255676.cpp:5:32: error: non-local lambda expression cannot have a capture-default
    5 | auto create_dynamic_wrapper = [&](const std::string_view name, T value) {
      |                                ^

Похоже, нам не нужен захват по умолчанию, поэтому мы могли бы просто использовать [] вместо этого — но почему бы просто не написать функцию?

На самом деле, я только что проверил, и это также незаконно в C ++ 17, поэтому код недействителен в том виде, в котором он представлен.


    DynamicWrapper() = default;

Для чего нужен этот конструктор по умолчанию? Мы никогда этим не пользуемся.


    DynamicWrapper(const std::string_view name_in, T value) : name_{ name_in } {
        this->pData_ = new T{ value };
    }

Почему вы назначаете pData а не просто инициализировать его, как вы name? Кроме того, делая name_in параметр const заставляет нас копировать его в name, а не возможность его переместить. Я бы написал:

    DynamicWrapper(std::string name, T value)
        : pData_{new T{std::move(value)}},
          name_{std::move(name)}
    {
    }

    ~DynamicWrapper() {
        if (nullptr != this->pData_) {
            delete this->pData_;
            this->pData_ = nullptr;
        }
    }

Здесь много беспорядка. Во-первых, мы можем отбросить все this-> мусор, который только делает код менее читабельным. Почему мы сравниваем с нулевым указателем? Если pData_ имеет значение null, то в тесте нет необходимости, потому что delete в любом случае ничего не будет делать. И это мертвая задача pData_, потому что объект выходит за пределы области видимости. Все это можно заменить более простым деструктором:

    ~DynamicWrapper() {
        delete pData_;
    }

Однако я считаю, что нам следует пересмотреть тип pData_. Так как g++ -Weffc++ предупреждает нас, что у нас есть элемент данных-указатель, но нет конструктора копирования и оператора присваивания копии, что делает этот класс опасным для использования.

Если мы заменим pData_ с умным указателем, то созданный компилятором (или удаленный компилятором) конструктор, деструктор и присваивание будут делать только то, что нужно (это Правило нуля).

Вот более простая версия того же:

#include <memory>
#include <string>

template<typename T>
auto create_dynamic_wrapper(std::string name, T value)
{
    class DynamicWrapper {
    public:
        DynamicWrapper(std::string name, T value)
            : value_{new T{std::move(value)}},
              name_{std::move(name)}
        {
        }

        auto value() const { return *value_; }
        auto const& ptr() const { return value_; }
        auto name() const { return name_; }
    private:
        const std::shared_ptr<T> value_;
        const std::string name_;
    };
    return DynamicWrapper(std::move(name), std::move(value));
}

Но я все еще не вижу в этом смысла.

  • Я знаю, что мог бы просто написать функцию. Не в этом был смысл моей разработки кода. Дело в том, что я смог создать класс, существующий только в области лямбда-выражения, через его объявление и определение. Затем, когда лямбда вызывается в некоторой единице перевода, содержащийся в нем тип класса все еще создается и возвращается через auto. Это больше о pattern и его поведение. Я прямо заявил, что меня не волнует smart_pointers поскольку я работал внутри компании и напрямую с new и delete. Я пометил C ++ 17, а не C ++ 20. Я использовал только объект-оболочку в качестве иллюстрации.

    — Фрэнсис Куглер

  • В коде Psudeo и проще говоря: auto obj = [](params...){ class Obj { pubic: members; } Obj o(params...); return o;}; тогда obj относится к типу Obj… Это образец, который меня беспокоит. Не как взять это и сделать из этого функцию! Идея состоит в том, чтобы иметь возможность использовать лямбда для создания экземпляра класса извне из класса, который он объявляет и определяет внутри. Единственное место, где существует класс, — это внутри этого лямбда-объекта … а не в глобальном пространстве имен файла … по крайней мере, пока вы не вызовете лямбда.

    — Фрэнсис Куглер


  • 1

    Да, я видел тег C ++ 17 и сначала подумал, что предупреждаю о том, что вы сделали, что несовместимо с C ++ 20. Затем я обнаружил, что он уже сломан в C ++ 17. Я до сих пор не понимаю, почему вы считаете, что именованная лямбда лучше, чем обычная функция — в чем преимущество?

    — Тоби Спейт

  • Я использую Visual Studio 2017, и у меня все заработало без проблем

    — Фрэнсис Куглер

  • 1

    @FrancisCugler Visual Studio не всегда соответствует стандартам. Если вы хотите, чтобы код переносился на системы, отличные от Windows, вам необходимо придерживаться стандартов.

    — pacmaninbw

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *