找到你要的答案

Q:C++ CRTP based dataflow output class design simplification 1 Looking up a name in an incomplete class 2 declval -> getOutput does not name a function template 3 Derived class member functions hide base class member functions 4 A solution that does not require a using-declaration

Q:C++在数据流输出级设计简化的基础 1在一个不完整的班级里查找一个名字 2 declval ->输出没有指定一个函数模板 3派生类成员函数隐藏基类成员函数 4一个不需要使用声明的解决方案


Background information

I am working on a dataflow-like design pattern. The classes presented below are meant to represent the output data dispatch mechanism. level1 is a CRTP base class. getOutput<N> in level1 is the function that can be used for getting the output data from an instance of a derived class. Depending on the template parameter N it calls one of the user defined methods getOutputImpl. These methods are meant to be provided in a (CRTP-style) derived class. Each of the methods getOutputImpl defines an output port associated with the user defined derived class. The input type of the method getOutputImpl is defined by design. The output types of the methods getOutputImpl can vary. However, by design, the output type must have a structure std::unique_ptr<TOutputType>, where TOutputType can be any class. More background information can be found here: previous question.


Question

To allow for automatic recognition of the number of the user defined ports (i.e. methods getOutputImpl) a method getOutputPortsNumber(void) is provided in the base level1 class. This method is based around the idea that the return type of all user defined methods getOutputImpl is std::unique_ptr<TOutputType>. Thus, one can define an additional getOutputImpl method in the base class that does not have this return type (e.g. it has a void return type: void getOutputImpl(...)).

The methodology described above works if the void getOutputImpl(...) is defined within the user-defined derived class (DataflowOutputClass in this example) along with other user-defined std::unique_ptr<TOutputType> getOutputImpl(...) methods. However, when the additional void getOutputImpl(...) method is moved to the base level1 class, I get a compilation error: no matching function for call to 'DataflowOutputClass<int>::getOutputImpl(PortIdxType<2ul>, const PolyIndex&) const.


Code

typedef size_t Index;
typedef unsigned long Natural;
typedef std::vector<Index> PolyIndex;
typedef const PolyIndex& crPolyIndex;
template<Index N> struct PortIdxType{};

template<typename TLeafType>
class level1
{

public:

    TLeafType* asLeaf(void)
        {return static_cast<TLeafType*>(this);}

    TLeafType const* asLeaf(void) const
        {return static_cast<TLeafType const*>(this);}

    template <Index N>
    auto getOutput(crPolyIndex c_Idx) const
        {return asLeaf() -> getOutputImpl(PortIdxType<N>{}, c_Idx);}

    static constexpr Natural getOutputPortsNumber(void)
        {return getOutputPortsNumberImpl<0>();}

    template<Index N>
    static constexpr std::enable_if_t<
        std::is_void<
            decltype(
                std::declval<TLeafType*>() ->
                getOutput<N>(PolyIndex({}))
                )
        >::value,
            Index
            > getOutputPortsNumberImpl(void)
        {return N;}

    template<Index N>
    static constexpr std::enable_if_t<
        !std::is_void<
            decltype(
                std::declval<TLeafType*>() ->
                getOutput<N>(PolyIndex({}))
                )
        >::value,
            Index
            > getOutputPortsNumberImpl(void)
        {return getOutputPortsNumberImpl<N + 1>();}

    template<Index N>
    void getOutputImpl(
        PortIdxType<N>, crPolyIndex c_Idx
        ) const
        {throw std::runtime_error("Wrong template argument.");}


};

template<typename T>
class DataflowOutputClass:
    public level1<DataflowOutputClass<T>>
{
public:

    // if void getOutputImpl(...) const is moved here from level1,
    // then the code compiles and works correctly.

    //overload for when N = 0
    std::unique_ptr<double> getOutputImpl(
        PortIdxType<0>, crPolyIndex c_Idx
        ) const
    {
        std::unique_ptr<double> mydouble(new double(10));
        return mydouble;
    }

    //overload for when N = 1
    std::unique_ptr<int> getOutputImpl(
        PortIdxType<1>, crPolyIndex c_Idx
        ) const
    {
        std::unique_ptr<int> myint(new int(3));
        return myint;
    }

};


int main()
{
    DataflowOutputClass<int> a;
    std::cout << a.getOutputPortsNumber() << std::endl;
}

背景信息

我工作在一个数据流设计模式。下面介绍的类代表输出数据调度机制。一是在基类。输出& lt;N >;在一级,可用于从派生类的一个实例得到输出数据的功能。根据模板参数n它调用一个用户定义的方法getoutputimpl。这些方法都是设置在(CRTP风格)的派生类。每一种方法getoutputimpl定义用户定义的派生类相关的输出端口。该方法getoutputimpl输入类型定义的设计。该方法getoutputimpl输出类型可以不同。然而,通过设计,输出类型,必须要有一个结构std::unique_ptr <;toutputtype >;,TOutputType在哪里可以被任何类。更多的背景信息可以在这里找到:以前的问题。


问题

为了能够对用户定义的端口号码自动识别(即方法getoutputimpl)方法getoutputportsnumber(void)是一类提供基础。该方法是基于的想法,所有用户定义的方法getoutputimpl返回类型是std::unique_ptr <;toutputtype & gt;。因此,可以定义在基类中没有这额外的getoutputimpl方法返回类型(例如具有void返回类型:无效getoutputimpl(…))。

上述方法如果无效getoutputimpl(…)在用户定义的派生类定义的(比如DataflowOutputClass)以及其他自定义std::unique_ptr <;toutputtype >;getoutputimpl(…)方法。然而,当附加孔隙getoutputimpl(…)方法移动到基地一级班,我得到一个编译错误:没有匹配的函数调用的dataflowoutputclass <;int >;::getoutputimpl(portidxtype <;2ul >;,const常量均匀&;)。


代码

typedef size_t Index;
typedef unsigned long Natural;
typedef std::vector<Index> PolyIndex;
typedef const PolyIndex& crPolyIndex;
template<Index N> struct PortIdxType{};

template<typename TLeafType>
class level1
{

public:

    TLeafType* asLeaf(void)
        {return static_cast<TLeafType*>(this);}

    TLeafType const* asLeaf(void) const
        {return static_cast<TLeafType const*>(this);}

    template <Index N>
    auto getOutput(crPolyIndex c_Idx) const
        {return asLeaf() -> getOutputImpl(PortIdxType<N>{}, c_Idx);}

    static constexpr Natural getOutputPortsNumber(void)
        {return getOutputPortsNumberImpl<0>();}

    template<Index N>
    static constexpr std::enable_if_t<
        std::is_void<
            decltype(
                std::declval<TLeafType*>() ->
                getOutput<N>(PolyIndex({}))
                )
        >::value,
            Index
            > getOutputPortsNumberImpl(void)
        {return N;}

    template<Index N>
    static constexpr std::enable_if_t<
        !std::is_void<
            decltype(
                std::declval<TLeafType*>() ->
                getOutput<N>(PolyIndex({}))
                )
        >::value,
            Index
            > getOutputPortsNumberImpl(void)
        {return getOutputPortsNumberImpl<N + 1>();}

    template<Index N>
    void getOutputImpl(
        PortIdxType<N>, crPolyIndex c_Idx
        ) const
        {throw std::runtime_error("Wrong template argument.");}


};

template<typename T>
class DataflowOutputClass:
    public level1<DataflowOutputClass<T>>
{
public:

    // if void getOutputImpl(...) const is moved here from level1,
    // then the code compiles and works correctly.

    //overload for when N = 0
    std::unique_ptr<double> getOutputImpl(
        PortIdxType<0>, crPolyIndex c_Idx
        ) const
    {
        std::unique_ptr<double> mydouble(new double(10));
        return mydouble;
    }

    //overload for when N = 1
    std::unique_ptr<int> getOutputImpl(
        PortIdxType<1>, crPolyIndex c_Idx
        ) const
    {
        std::unique_ptr<int> myint(new int(3));
        return myint;
    }

};


int main()
{
    DataflowOutputClass<int> a;
    std::cout << a.getOutputPortsNumber() << std::endl;
}
answer1: 回答1:

In the original code, I identified three issues:

  1. std::declval<TLeafType*>() -> getOutput tries to look up a name in an incomplete class.

  2. std::declval<TLeafType*>() -> getOutput<N> does not name a function template getOutput.

  3. The getOutputImpl declarations in the derived class hide any member functions with the same name of the base class.


1 Looking up a name in an incomplete class

The expression std::declval<TLeafType*>() -> getOutput is used in the return type of DataflowOutputClass::getOutputPortsNumberImpl.

Instantiating a class template leads to the instantiation of the declarations of all member functions. When you derive with CRTP via level1<DataflowOutputClass<T>> in the DataflowOutputClass class, the compiler needs to instantiate level1<..> before instantiating the derived class. Therefore, during the instantiation of level1<DataflowOutputClass<T>>, the DataflowOutputClass<T> class is still incomplete.

A workaround is to postpone the determination of the return type of DataflowOutputClass::getOutputPortsNumberImpl by making it dependent on a template parameter of the function template:

template<Index N, typename T = TLeafType>
static constexpr std::enable_if_t<
    std::is_void<
        decltype(
            std::declval<T*>() ->
            getOutput<N>(PolyIndex({}))
            )
    >::value,
        Index
        > getOutputPortsNumberImpl(void)
    {return N;}

Now, the return type is dependent on a template-parameter of the function template. This return type can only be resolved when the function is instantiated. The function is instantiated implicitly via the usage of getOutputPortsNumber in main, where the derived class is already complete.

Note that it is not necessary to look up the name getOutput in the scope of the derived class, you can as well default T = level1. We would not look up a name in the derived class if we used:

template<Index N, typename T = TLeafType>
static constexpr std::enable_if_t<
    std::is_void<
        decltype(
            getOutput<N>(PolyIndex({}))
            )
    >::value,
        Index
        > getOutputPortsNumberImpl(void)
    {return N;}

However, to determine the return type of this getOutputPortsNumberImpl, instantiating the definition of getOutput is necessary, because getOutput uses return type deduction. Its definition will suffer from a similar problem as the original code: it tries to look up a name in an incomplete type.

Fixing the issue with return type deduction by making the return type of the calling function template dependent on a function template parameter seems like a poor fix to me, but the whole technique can be replaced by something simpler, see below.


2 declval<TLeafType*> -> getOutput<N> does not name a function template

In #1, we have already replaced this with std::declval<T*>() -> getOutput<N>(PolyIndex({})), but the issue is the same. Consider:

bool operator> (bool, PolyIndex);

// class template level1

struct derived
{
    int getOutput;
};

With this setup, an expression like declval<T*>() -> getOutput<N>(PolyIndex({})) can be parsed as:

(
  (declval<T*>()->getOutput)  <  N
)
>
(
  PolyIndex({})
)

That is, (x < N) > PolyIndex{}.

To allow the compiler to understand that getOutput is a template, use the template keyword:

std::declval<T*>() -> template getOutput<N>(PolyIndex{})

(The additional () to initialize PolyIndex are unnecesary.)


3 Derived class member functions hide base class member functions

In general, any member in a derived class hides members of the same name of the base classes. To overload a member function in a derived class with member functions in a base class, you can use a using-declaration to "inject" the name of the base member into the derived class:

template<typename T>
class DataflowOutputClass:
    public level1<DataflowOutputClass<T>>
{
public:

    using level1<DataflowOutputClass<T>>::getOutputImpl;

    //overload for when N = 0
    std::unique_ptr<double> getOutputImpl(
        PortIdxType<0>, crPolyIndex c_Idx
        ) const;

    // ...
};

4 A solution that does not require a using-declaration

Currently, a using-declaration is required since the OP's metaprogramming must produce a valid return type for the expression getOutput<N>(PolyIndex({})). The current approach differentiates void from non-void return types. Instead, we can simply detect if the expression getOutput<N>(PolyIndex{}) is well-formed. For this, I'll use Walter E. Brown's void_t technique:

template<typename T>
struct voider { using type = T; };

template<typename T>
using void_if_well_formed = typename voider<T>::type;

We will use this as follows:

void_if_well_formed< decltype(expression) >

will yield the type void if the expression is well-formed. Otherwise, if the expression is ill-formed due to a substitution failure in the immediate context, the whole void_if_well_formed<..> will produce a substitution failure in the immediate context. These kinds of errors can be used by a technique called SFINAE: Substitution Failure Is Not An Error. It might be more appropriately be named Substitution Failure In The Immediate Context Is Not An Error.

SFINAE can be exploited e.g. by declaring two function templates. Let expression<T>() stand for any expression that is dependent on T.

template<typename T, void_if_well_formed<decltype(expression<T>())>* = nullptr>
std::true_type  test(std::nullptr_t);

template<typename T>
std::false_type test(void*);

If we now call the test via test<some_type>(nullptr), the first overload is preferred because the argument types matches exactly the function parameter type. However, the second overload is also viable. If the first overload is ill-formed due to SFINAE, it is removed from the overload set and the second overload is chosen instead:

template<typename T>
using test_result = decltype( test<T>(nullptr) );

Using these techniques, we can implement level1 as follows:

template<typename TLeafType>
class level1
{
public:
    template <Index N, typename T = TLeafType>
    using output_t =
        decltype(std::declval<T*>() ->
                 getOutputImpl(PortIdxType<N>{}, std::declval<crPolyIndex>()));

    static constexpr Natural getOutputPortsNumber(void)
        {return getOutputPortsNumberImpl<0>(nullptr);}

    template<Index N>
    static constexpr Index getOutputPortsNumberImpl(void*)
        {return N;}

    template<Index N, typename T = TLeafType,
             void_if_well_formed<output_t<N, T>>* = nullptr>
    static constexpr Index getOutputPortsNumberImpl(std::nullptr_t)
        {return getOutputPortsNumberImpl<N + 1>(nullptr);}

};

With slightly more work, we can even write this as follows:

template<Index N>
struct HasOutputFor
{
    static auto P() -> PortIdxType<N>;
    static auto cr() -> crPolyIndex;

    template<typename T>
    static auto requires_(T&& t) -> decltype(t.getOutputImpl(P(), cr()));
};

template<Index N, typename T = TLeafType, REQUIRE( HasOutputFor<N>(T) )>
static constexpr Index getOutputPortsNumberImpl(std::nullptr_t);

在原代码中,我确定了三个问题:

  1. std::declval <;tleaftype * & gt;()- >;输出要查找一个名字在一个不完整的类。

  2. std::declval <;tleaftype * & gt;()- >;输出& lt;N >;没有指定一个函数模板的输出。

  3. 在派生类中声明的getoutputimpl隐藏任何成员函数与基类相同的名字。


1在一个不完整的班级里查找一个名字

表达std::declval <;tleaftype * & gt;()- >;输出采用的是DataflowOutputClass的返回类型::getoutputportsnumberimpl。

实例化类模板的实例化的所有成员函数声明。当你获得通过一级在dataflowoutputclass <;<;T >;>;在dataflowoutputclass类,编译器需要实例化一级<;.. >;在实例化派生类。因此,对一级& lt实例化过程中;dataflowoutputclass <;T >;>;,这dataflowoutputclass <;T & gt;类仍然是不完整的。

一个办法是推迟DataflowOutputClass的返回类型的测定:通过使它依赖于一个函数模板的模板参数getoutputportsnumberimpl:

template<Index N, typename T = TLeafType>
static constexpr std::enable_if_t<
    std::is_void<
        decltype(
            std::declval<T*>() ->
            getOutput<N>(PolyIndex({}))
            )
    >::value,
        Index
        > getOutputPortsNumberImpl(void)
    {return N;}

现在,返回类型依赖于函数模板的模板参数。这个返回类型只能在该函数被实例化时。该功能是通过隐式实例化getoutputportsnumber主要的用法,在派生类中已经完成。

请注意,这是没有必要查对派生类的作用域的名称的输出,你也可以默认T =一级。如果我们使用的话,我们不会在派生类中查找一个名称:

template<Index N, typename T = TLeafType>
static constexpr std::enable_if_t<
    std::is_void<
        decltype(
            getOutput<N>(PolyIndex({}))
            )
    >::value,
        Index
        > getOutputPortsNumberImpl(void)
    {return N;}

然而,要确定这getoutputportsnumberimpl返回类型实例化的输出的定义是必要的,因为输出使用返回类型推导。它的定义将受到与原代码类似的问题:它试图查找一个不完整类型的名称。

通过将调用函数模板的返回类型依赖于函数模板参数来修复返回类型的问题,对我来说似乎是一个不好的解决方法,但是整个技术可以被一些简单的东西所代替,见下面。


2 declval<TLeafType*> -> getOutput<N> does not name a function template

在# 1,我们已经取代了std::declval <;T * & gt;()- >;输出& lt;N >;(均匀({ })),但问题是相同的。考虑:

bool operator> (bool, PolyIndex);

// class template level1

struct derived
{
    int getOutput;
};

采用这种装置,一个表达式declval <;T * & gt;()- >;输出& lt;N >;(均匀({ }))可以解析为:

(
  (declval<T*>()->getOutput)  <  N
)
>
(
  PolyIndex({})
)

即(x & lt;n)& gt;均匀{ }。

让编译器知道的输出是一个模板,模板使用的关键词:

std::declval<T*>() -> template getOutput<N>(PolyIndex{})

(附加()初始化均匀是不必要的。)


3派生类成员函数隐藏基类成员函数

一般来说,派生类中的任何成员都隐藏基类的相同名称的成员。在基类中的成员函数重载派生类中的成员函数时,可以使用“使用声明”将基类成员的名称“注入”到派生类中:

template<typename T>
class DataflowOutputClass:
    public level1<DataflowOutputClass<T>>
{
public:

    using level1<DataflowOutputClass<T>>::getOutputImpl;

    //overload for when N = 0
    std::unique_ptr<double> getOutputImpl(
        PortIdxType<0>, crPolyIndex c_Idx
        ) const;

    // ...
};

4一个不需要使用声明的解决方案

目前,使用需要声明由于OP的元编程必须产生表达输出& lt一个有效的返回类型;N >;(均匀({ }))。当前方法区分无效和非无效返回类型。相反,我们可以简单的检测,如果表达式输出& lt;N >;(均匀{ })是良好的。为此,我要用Walter E. Brown的void_t技术:

template<typename T>
struct voider { using type = T; };

template<typename T>
using void_if_well_formed = typename voider<T>::type;

我们将使用如下:

void_if_well_formed< decltype(expression) >

如果表达式形式良好,将产生类型无效。否则,如果表达式不形成由于在即时语境替换失败,整个void_if_well_formed <;.. >;会在当下产生一个替代的失败。这些类型的错误可以通过一种称为sfinae用于替换故障是不是一个错误。它可能更恰当地命名为替代失败,在目前的情况下是不是一个错误。

sfinae可以利用,例如声明两个函数模板。让表达& lt;T & gt;()站在任何表达,是依赖于T。

template<typename T, void_if_well_formed<decltype(expression<T>())>* = nullptr>
std::true_type  test(std::nullptr_t);

template<typename T>
std::false_type test(void*);

如果我们现在称之为测试通过测试& lt;some_type >;(nullptr),第一个是首选因为参数类型完全匹配的函数参数类型。然而,第二超载也是可行的。如果第一个过载是由于sfinae病形成的,它是从超载集和第二过载删除选择:

template<typename T>
using test_result = decltype( test<T>(nullptr) );

使用这些技术,我们可以实现一级如下:

template<typename TLeafType>
class level1
{
public:
    template <Index N, typename T = TLeafType>
    using output_t =
        decltype(std::declval<T*>() ->
                 getOutputImpl(PortIdxType<N>{}, std::declval<crPolyIndex>()));

    static constexpr Natural getOutputPortsNumber(void)
        {return getOutputPortsNumberImpl<0>(nullptr);}

    template<Index N>
    static constexpr Index getOutputPortsNumberImpl(void*)
        {return N;}

    template<Index N, typename T = TLeafType,
             void_if_well_formed<output_t<N, T>>* = nullptr>
    static constexpr Index getOutputPortsNumberImpl(std::nullptr_t)
        {return getOutputPortsNumberImpl<N + 1>(nullptr);}

};

有了更多的工作,我们甚至可以写如下:

template<Index N>
struct HasOutputFor
{
    static auto P() -> PortIdxType<N>;
    static auto cr() -> crPolyIndex;

    template<typename T>
    static auto requires_(T&& t) -> decltype(t.getOutputImpl(P(), cr()));
};

template<Index N, typename T = TLeafType, REQUIRE( HasOutputFor<N>(T) )>
static constexpr Index getOutputPortsNumberImpl(std::nullptr_t);
c++  c++11  design-patterns  crtp  dataflow