Реализация потока <T>, который может быть преобразован в поток <U>, где U - основание T - PullRequest
0 голосов
/ 28 апреля 2018

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

Интерфейс потока должен выглядеть следующим образом:

template <typename T>
class Stream {
public:
    T* input();
}

При каждом вызове input() возвращает следующий объект в потоке или нулевой указатель, если поток пустой.

Проблема в том, что я хотел бы, чтобы Stream<T> был конвертируемым в Stream<U>, если T* конвертируемым в U*.

Моя неудачная попытка состояла в том, чтобы использовать указатель на реализацию следующим образом:

class StreamImplBase {
public:
    virtual void* input_raw() = 0;
}

template <typename T>
class StreamImpl: public StreamImplBase {
public:
    void* input_raw() final { return input(); }
    virtual T* input() = 0;
}

template <typename T>
class Stream {
    StreamImplBase* impl;
public:
    Stream(StreamImpl<T>* impl): impl(impl) {}
    T* input() { return static_cast<T*>(impl->input_raw()); }
}

Конструктор из StreamImpl<T> гарантирует, что void*, возвращенный из input_raw(), был получен путем приведения T к void*, поэтому static_cast<T*> безопасен.

Однако, если я выполню какое-либо преобразование, это утверждение не будет верным. То есть строительство Stream<T> из StreamImpl<U> небезопасно, даже если U* конвертируется в T*.

Итак, мой вопрос, как мне решить эту проблему?

Я вижу следующие возможности:

  • сохранить конвертер (например, std::function<T*(void*)>) в потоке и обновлять его при каждом приведении. Это кажется излишне дорогим;

  • сохранить результат static_cast<U*>((T*)0) и добавить этот результат к указателю, полученному из input_raw(). Это кажется излишне опасным;

  • добавьте второй параметр шаблона OrigT и сохраните StreamImpl<OrigT>* вместо сохранения StreamImplBase*. Это ограничит возможные применения класса, которых я бы хотел избежать;

  • с использованием dynamic_cast не вариант, потому что нельзя dynamic_cast с void*.

Есть ли другие возможности? Как другие реализуют что-то подобное?


Вот пример использования. Предположим, у нас есть сообщение protobuf X. Я бы хотел, чтобы это работало:

Stream<X> stream = ...;
Stream<google::protobuf::Message> raw_stream = stream;

Опять же, я не знаю, как реализовано Stream<X>. Все, что я знаю, это то, что он содержит общий указатель на некоторую реализацию, которая генерирует сообщения.

Ответы [ 2 ]

0 голосов
/ 28 апреля 2018

Существует одна функция C ++, которая позволяет преобразовывать производный класс в базовый класс, когда два класса не известны в одной и той же функции: исключения. Естественно, это безобразное, безобразное злоупотребление, но оно работает:

#include <type_traits>
#include <stdexcept>

class StreamImplBase {
public:
    virtual void toss_input() = 0;
};

template <typename T>
class StreamImpl : public StreamImplBase {
public:
    virtual T* input() = 0;
    void toss_input() override
    { throw input(); }
};

template <typename T>
class Stream {
    StreamImplBase* impl;
public:
    template <typename U,
        std::enable_if_t<std::is_convertible<U*, T*>::value>* = nullptr>
    explicit Stream(StreamImpl<U>* impl) : impl(impl) {}

    template <typename U,
        std::enable_if_t<std::is_convertible<U*, T*>::value>* = nullptr>
    Stream(const Stream<U>& str) : impl(str.impl) {}

    T* input() const
    {
        try {
            impl->toss_input();
        } catch (T* ptr) {
            return ptr;
        }
        throw std::logic_error("Stream logic is broken?");
    }
};

См. полный пример использования coliru . Это можно улучшить, используя std::shared_ptr<StreamImpl<U>> и std::shared_ptr<StreamImplBase>.

0 голосов
/ 28 апреля 2018

Это:

template <typename T>
class Stream {
public:
  T* input();
};

- это объект с одной операцией, который принимает 0 аргументов и возвращает T*.

Так это:

std::function<T*()>

по общему признанию, вы вызываете его как stream() вместо stream.input().

С этим вторым решением, если U является основанием T, то вы можете преобразовать вышеупомянутое значение в std::function<U*()>. Что решает вашу проблему.

Лично я не думаю, что ввод .input между названием вашего потока и () стоит большой работы.

Стирание типа, которое уже сделал кто-то другой, является лучшим типом стирания.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...