динамическое приведение переменных аргументов к шаблонам


У меня есть приложение C++, которое выполняет тестовые случаи. Возможно, что некоторые тестовые случаи будут зависеть от выходных данных других тестовых случаев.

Все тестовые случаи реализуют базовый интерфейс:

/// base class for all test cases
class ITest
{
public:
    virtual void Execute() = 0;
};

Тестовые случаи, которые производят некоторый объект, который может быть полезен для других тестовых случаев, реализуют этот интерфейс:

/// implemented by test cases that provide data to other test cases
template< class Obj >
class IDependency
{
public:
    virtual Obj Get() = 0;
};

Тестовые случаи, требующие данных из других тестовых случаев, реализуют этот интерфейс:

/// implemented by test cases that require data from other test cases
template< class Obj >
class IDependent
{
public:

    void SetDependency( IDependency< Obj >* dependency )
    {
        dependency_ = dependency;
    };

protected:
    Obj GetDependency() const
    {
        return dependency_->Get();
    };

private:
    IDependency< Obj >* dependency_;
};

Два примера тестов. Один требует объекта const wchar_t; один производит этот объект:

/// A test case that provides a "const wchar_t*" object to other test cases
class Foo : public ITest, 
            public IDependency< const wchar_t* >
{
public:
    const wchar_t* Get() 
    { 
        if( object_.length() == 0 )
            Execute();
        return object_.c_str();
    };

    virtual void Execute()
    {
        printf( "Execute Foon" );
        object_ = L"Object produced by Foo";
    };

private:
    std::wstring object_;
};

/// A test case that first requires a "const wchar_t*" object
class Bar : public ITest,
            public IDependent< const wchar_t* >
{
public:

    virtual void Execute()
    {
        const wchar_t* needed_object = GetDependency();

        printf( "Execute Bar with %Sn", needed_object );
    };
};

Тестовые случаи хранятся в виде списка. Случаи добавляются в список в процессе регистрации:

/// List of test cases to execute
std::vector< ITest* > list_;

/// Register a test case to execute with the system
void Register( ITest* test_case )
{
    list_.push_back( test_case );
}

Вот моя проблема. Я хотел реализовать перегрузку функции ' Register ()', которая также принимает зависимости. Но, поскольку зависимости могут быть любого типа (не только "const wchar_t*" из этого примера), я не уверен, как это сделать. Ниже приведен пример более или менее того, что я ищу, но я не уверен, как это сделать работа.

/// Register a test case with dependencies with the system
void Register( ITest* test_case, ITest* dependency, ... )
{
    IDependent< ??? >* dependent = dynamic_cast< IDependent< ??? >* >( test_case );
    IDependency< ??? >* dep = dynamic_cast< IDependency< ??? >* >( dependency );

    va_list dep_list;
    for( va_start( dep_list, dependency ); 
         NULL != dep; 
         dep = dynamic_cast< IDependency< ??? >* >( va_arg( dep_list, ITest* ) ) )
    {
        dependent->SetDependency( dep );
    }
    va_end( dep_list );

    Register( test_case );
}

Пример использования:

int _tmain( int argc, _TCHAR* argv[] )
{
    /// Test case Foo
    Foo foo;

    /// Test case bar (depends on Foo)
    Bar bar;

    /// Register test case Bar with a dependency on Foo
    Register( &bar, &foo );

    /// Execute Bar. Because it depends on Foo, that will be executed first
    list_->begin()->Execute();
    return 0;
}

Ожидаемый результат:

Execute Foo
Execute Bar with Object produced by Foo

Есть ли у кого-нибудь предложения о том, как я могу успешно реализовать эту архитектуру? (или лучшая архитектура, которая действительно работает?)

Спасибо, Paul

2 4

2 ответа:

Я вижу два возможных решения.

Статика

Сделайте метод Register() шаблоном. Простым решением было бы ограничить число зависимостей до некоторого разумного максимума.

template <class T, class D1>
void Register(T* test_case, IDependency<D1>* d1)
{
    BOOST_STATIC_ASSERT(boost::is_base_and_derived<IDependent<D1>, T>::value);
    // since we now know that T is a IDependent<D1>, a dynamic_cast would only be necessary
    // to allow virtual inheritance.
    static_cast<IDependent<D1>*>(test_case)->SetDependency(d1);
    Register(test_case);
}

template <class T, class D1, class D2>
void Register(T* test_case, IDependency<D1>* d1, IDependency<D2>* d2)
{
    BOOST_STATIC_ASSERT(boost::is_base_and_derived<IDependent<D1>, T>::value);
    static_cast<IDependent<D1>*>(test_case)->SetDependency(d1);
    Register(test_case, d2);
}

template <class T, class D1, class D2, class D3>
void Register(T* test_case, IDependency<D1>* d1, IDependency<D2>* d2, IDependency<D3>* d3)
{
    BOOST_STATIC_ASSERT(boost::is_base_and_derived<IDependent<D1>, T>::value);
    static_cast<IDependent<D1>*>(test_case)->SetDependency(d1);
    Register(test_case, d2, d3);
}

// ...

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

В качестве альтернативы вы можете сделать Register() возвращающим прокси-класс, так что вы можете написать что-то вроде это:

Register(test_case)(dep1)(dep2)(dep3) /* ... */ (depN);
Прокси-класс будет хранить указатель на контейнер и тестовый случай и определять оператор вызова функции, который выглядит точно так же, как функция Register(T* test_case, IDependency* d1) в примере выше, только без аргумента "test_case" и окончательного вызова Register(test_case) (что можно сделать в dtor прокси-класса).

Динамический

Если я правильно понимаю, что вы пытаетесь сделать, каждая "зависимость" может порождать только один тип зависимости. результат. В этом случае вы можете изменить интерфейс IDependency следующим образом:

class IDependencyBase
{
public:
    virtual void ApplyTo(ITest* target) = 0;
};

template <class T>
class IDependency : public IDependencyBase
{
public:
    virtual void ApplyTo(ITest* target)
    {
        // cast to reference gives us an std::bad_cast if the types are not compatible,
        // which I think is a good thing here
        dynamic_cast<IDependancy<T>&>(*target).SetDependancy(this); 
    }

    virtual T Get() = 0;
};

template <class InputIterator>
void Register(ITest* test_case, InputIterator begin, InputIterator end)
{
    for (; begin != end; ++begin)
    {
        IDependancyBase* dep = *begin;
        dep->ApplyTo(test_case);
    }
    Register(test_case);
}

template <class Container>
void Register(ITest* test_case, Container deps)
{
    Register(test_case, deps.begin(), deps.end());
}

Теперь вам может показаться заманчивым снова реализовать свое решение varargs, что-то вроде этого (продолжение второго примера):

void Register(ITest* test_case, ...)
{
    va_list dep_list;
    va_start(dep_list, test_case);
    while(IDependencyBase* dep = va_arg(dep_list, ITest*))
        dep->ApplyTo(test_case);

    va_end(dep_list);

    Register( test_case );
}

// and use it like

int _tmain( int argc, _TCHAR* argv[] )
{
    Foo foo;
    Bar bar;

    Register(&foo);
    Register(&bar, &foo, 0);

    list_->begin()->Execute();
    return 0;
}

Однако это не гарантирует, что сработает. В приведенном выше коде Foo * хранится как аргумент varagrs и считывается как IDependencyBase*. Это не гарантирует работу, так как ни Foo, ни IDependencyBase не являются стручками (IIRC они оба должны быть стручками чтобы это гарантированно сработало - возможно, это не гарантировано даже тогда, я должен был бы посмотреть его в стандарте). Это не какая-то надуманная "не гарантированная стандартом, но будет работать везде" вещь. Введите множественное и / или виртуальное наследование, и это почти гарантированно провалится.

Поэтому общий совет при использовании C++: не используйте функции varargs, если нет другого способа. И всегда есть другой путь.

УГ. Сложный вопрос. Хорошо, сначала я попытаюсь объяснить , Почему шаблонный подход, который вы здесь используете, вызывает проблемы; а затем я попытаюсь предложить альтернативное решение.

Вот так.

Шаблоны-это своего рода следующая эволюция макросов. Они обладают большей безопасностью типов, но у них все еще есть несколько ограничений, которые становятся болезненно очевидными, когда вы пытаетесь связать их с объектами шаблона. Это становится грязным. Ключевая вещь здесь заключается в том, что когда вы объявляете класс:

template< class Obj > class IDependency

Ничего фактически созданы или определены. Здесь (пока) нет определенного класса, а именно, нет (и никогда не будет) реального, пригодного для использования класса с именем IDependency.

Когда вы пытаетесь использовать этот класс шаблона, затем компилятор будет создавать экземпляр шаблона, генерируя фактический, определенный класс (с некоторым конкретным именем, искажающим происходящее под обложками). То есть, когда вы говорите IDependency< const wchar_t* >, компилятор генерирует определение класса для const wchar_t* аромата этого шаблона, и у вас будет класс с именем что-то вроде IDependency_const_wchart_p за кулисами.

Это может звучать как какая-то скучная низкоуровневая деталь, но здесь это имеет значение по очень важной причине: IDependency<const wchar_t*> и IDependency<int> не имеют абсолютно ничего общего. Это не один и тот же класс, и у них нет общего базового типа.

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

Это довольно многословно, но это означает, что Если вы хотите динамически привести к IDependency, вы не можете. Вы можете только динамически привести к созданию экземпляра IDependency.

Например: это законно:

IDependent< const wchar_t* >* dependent =
         dynamic_cast< IDependent< const wchar_t* >* >( test_case );
if (dependent) {
    IDependency< const wchar_t* >* dep =
         dynamic_cast< IDependency< const wchar_t* >* >( dependency );
}
else
{
    IDependent< const int >* dependent =
         dynamic_cast<IDependent<const int>* (test_case);
    if (dependent) {
       ...
    }
}
Очевидно, что это далеко не идеально. Вы можете очистите это немного, потянув его в метод шаблона (например, try_to_attach_dependency<T>), а затем повторно вызывая его с различными типами, пока это не удастся.

Другая проблема : вы пытаетесь присоединить кучу объектов IDependency к вашему объекту IDependent - для этого требуется, чтобы все объекты IDependency имели одинаковую специализацию шаблона (т. е. вы не можете смешивать с ). Вы можете проверить это во время выполнения (так как мы делаем динамический кастинг) и либо игнорировать различные единицы, или выдавать ошибку, если объекты IDependency не однородны. Но вы не можете применить это во время компиляции. Вот в чем опасность dynamic_cast.

Итак, что вы можете с этим поделать?Я думаю, что это та часть, которая тебя действительно волнует.

У вас есть несколько вариантов:

Можно создать фактический базовый класс IDependency. Это позволит вам передавать объекты IDependency*, что на первый взгляд кажется хорошей идеей, но проблема в том, что Метод Get. Вся причина, по которой это класс шаблона, заключается в том, что у вас может быть метод, который возвращает различные типы, в зависимости от того, какая специализация используется. Вы не можете сделать это с базовыми типами (без некоторой креативности).

Вместо метода Obj Get() можно использовать метод void Set(ITest*). Таким образом, вместо того, чтобы ITest запрашивал информацию у IDependency, вы заставляете IDependency сообщать ITest информацию. Делать это таким образом-это своего рода инверсия, но она позволяет создать не шаблонный базовый класс для классов IDependency. Я не знаю, как вы планировали использовать Get, поэтому этот механизм разворота может сработать для вас.

Можно такжеизбавиться от списка переменных аргументов . Va_args часто является плохой идеей (отсутствие безопасности), и, подняв ее до уровня вызывающего объекта, вы можете шаблонизировать функцию и сделать следующее:

template <typename T>
void Register( ITest* test_case, const std::vector< IDependency<T>* >& dependencies )
{
    IDependent<T>* dependent = static_cast< IDependent<T>* >( test_case );

    for( std::vector< IDependency<T>* >::const_iterator iter = dependencies.begin();
         iter != dependencies.end();
         ++iter)
    {
        dependent->SetDependency( *iter );
    }

    Register( test_case );
}

Затем вы можете использовать это, выполнив следующее:

std::vector< IDependency<const wchar_t*> > dependencies; // or whatever type you want
dependencies.push_back(&foo);
// ... more dependencies as needed
Register(&bar, dependencies);

Это накладывает немного больше усилий на вызывающий (то есть вы не можете просто сбросить все это в список, разделенный запятыми), но это работает, и это безопаснее. Обратите внимание, что теперь мы можем использовать static_cast, который проверяется во время компиляции.

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