在可变参数模板中使用填充程序的更简洁方法

More succinct way to use shims in variadic templates?

本文关键字:程序 填充 方法 简洁 变参 参数      更新时间:2023-10-16

C++模板通常被同化为膨胀的创建者,而Shim的想法正是处理这一点:使模板只是常规函数上的薄包装器。这是减少膨胀的好方法。

例如,让我们使用一个简单的填充程序:

//
// Shim interface
//
struct Interface {
    virtual void print(std::ostream& out) const = 0;
}; // struct Interface
std::ostream& operator<<(std::ostream& out, Interface const& i) {
    i.print(out);
    return out;
}
template <typename T>
struct IT: public Interface {
    IT(T const& t): _t(t) {}
    virtual void print(std::ostream& out) const { out << _t; }
    T const& _t;
};
template <typename T>
IT<T> shim(T const& t) { return IT<T>(t); }

现在,我可以像这样使用它:

void print_impl(Interface const& t);
template <typename T>
void print(T const& t) { print_impl(shim(t)); }

无论print_impl如何实现,print仍然非常轻量级,应该内联。简单易行。


然而,C++11引入了可变参数模板。然后,典型的冲动是使用C++11个可变参数模板重新实现所有不安全的C-可变参数,甚至维基百科也建议使用printf实现。

不幸的是,维基百科的实现不处理位置参数:允许您指定在那里打印第 3 个参数的那种,等等......如果我们有一个带有这个原型的功能,那会很容易:

void printf_impl(char const* format, Interface const* array, size_t size);

或类似。

现在,我们如何从原始接口桥接

template <typename... T>
void printf(char const* format, T const&... t);

到上面的签名 ?

填充程序的一个困难是,它们依赖于与 const-ref 行为的绑定来延长创建的临时包装器的生存期,而不必动态分配内存(如果这样做,它们不会便宜)。

虽然似乎很难一步获得绑定 + 数组转换。特别是因为语言中不允许引用数组(和指向引用的指针)。


对于那些感兴趣的人,我有一个解决方案的开始:

//
// printf (or it could be!)
//
void printf_impl(char const*, Interface const** array, size_t size) {
    for (size_t i = 0; i != size; ++i) { std::cout << *(array[i]); }
    std::cout << "n";
}
template <typename... T>
void printf_bridge(char const* format, T const&... t) {
    Interface const* array[sizeof...(t)] = { (&t)... };
    printf_impl(format, array, sizeof...(t));
}
template <typename... T>
void printf(char const* format, T const&... t) {
    printf_bridge(format, ((Interface const&)shim(t))...);
}

但是您会注意到引入了一个补充步骤,这有点烦人。不过,它似乎有效。

如果有人提出更好的实施方案,我将不胜感激。


@Potatoswatter建议使用初始值设定项列表,这有点帮助(那里没有范围)。

void printf_impl(char const*, std::initializer_list<Interface const*> array) {
    for (Interface const* e: list) { std::cout << *e; }
    std::cout << "n";
}
template <typename... T>
void printf_bridge(char const* format, T const&... t) {
    printf_impl(format, {(&t)...});
}

但仍然没有解决中间功能问题。

使其轻量级取决于消除类型参数化。您的填充程序可能会使用表达式 out << _t 实例化一些重型内容,因此它可能不是一个很好的示例。

C varargs 通过隐式将所有内容转换为 intptr_t 来处理问题。如果只想复制 C printf功能,则可以对 reinterpret_castinitializer_list 执行相同的操作。

template <typename... T>
void printf(char const* format, T const&... t) {
    printf_impl(format, { reinterpret_cast< std::intptr_t >( t ) ... } );
}

这显然是次优的,但填充程序本质上是有限的。如果需要,可以在initializer_list中使用多态类型执行其他操作。

无论如何,这正是initializer_list的目的。它只能从大括号的初始化列表构造,使其大小成为编译时常量。但大小只能作为运行时常量读回。因此,它唯一的实际用途是将仅在列表长度上不同的模板漏斗到常见的可变长度实现中。

再加上initializer_list参数的生命周期语义——对象是在堆栈上的一个连续数组中创建的,并在函数调用语句结束时死亡——initializer_list看起来很像<varargs>!(编辑:或您的解决方案,我现在实际上已经回去阅读:vP)

编辑:由于容器不能直接存储多态对象,并且智能指针不适合临时参数对象,因此实现多态性需要将指针指向临时对象。丑陋,但由于临时对象的生命周期保证,因此是合法的:

template <typename... T>
void printf(char const* format, T const&... t) {
    printf_impl(format, std::initializer_list< Interface const * >
        { & static_cast< Interface const & >( shim(t) )... } );
}

如果您可以使用同构(内存中相同的大小和对齐方式)类型,请查看以下内容:

// thin template layer over regular class/methods
template< typename T, typename... Contracts>
inline void Container::bindSingleAs(){
isMultiBase< T, Contracts...>(); //compile time test
    priv::TypeInfoP         types[ sizeof...( Contracts)]
                            { &typeid( Contracts)... };
    priv::SharedUpcastSignature upcasts[ sizeof...( Contracts)]
                            { &priv::shared_upcast< T, Contracts>... };
    // dispatch over non-template method.
    container->bindSingleAs( &typeid(T), types, upcasts, sizeof...(   Contracts));
}

现在由于评论而编辑后,我认为有 2 个相互冲突的必要条件。

  1. 想要数组参数
  2. 不想产生复制开销

如果 printf_impl 函数需要一个数组作为参数,那么这意味着数组元素在内存中应该具有相同的配置(这意味着如果 1 个元素是 64 字节,则强制所有其他元素以 64 字节对齐,即使它们是 1 个字节......)因此需要复制,或者至少复制到指向固定位置的指针, 所以绝对不可能做OP想要的。

我们仍然可以构建该数组,但我们受到约束

    我们
  1. 根本不想要复制,那么我们应该静态声明数组的类型,这迫使我们构建第三种类型。

    auto Array = MakeArray( /* values*/);

    printf( Array);

  2. 我们
  3. 接受复制,所以我们在函数内部构建数组,因为值未知,我们可以对用户隐藏数组,但我们必须将参数复制到固定内存位置,但是我们仍然将数组隐藏在后台。

  4. 堆分配,
  5. 允许在非常紧凑的数组中传递参数,但是参数必须驻留在其他地方,堆分配可能很昂贵。

第一个解决方案是通过创建一个静态类型数组来接受编码中的额外复杂性,该数组可以解决元素(全部与最大元素对齐),但是这是次优的,因为增加对象的大小无论如何都会影响性能(如果该数组即使在函数调用之后仍然存在)

第二种解决方案隐藏了模板接口背后的复杂性,但是它无法避免将值临时复制到与第一种解决方案相同的数组的性能成本。

所以,对不起,这是不可能的。另一个答案介于数字 2 和 3 之间。所有其他可能的答案都在 3 个类别之一中。