通用类型与显式类型类/结构

时间:2015-12-05 22:06:26

标签: c++

我想知道类/结构的泛型显式类型实现的影响。 [在效果代码/二进制文件]

例如,假设我想实现一个可以接受任何这些值类型(int,float,double)的元组结构。

有两种方法可以解决这个问题:

1 - 使用带模板的通用结构

template <class T>
struct tuple{
    T x,y;
    //... the rest of methods and operand implementations
};

2 - 明确地为每种类型实施副本

struct tuplef{
    float x,y;
    //... the rest of methods and operand implementations
};

struct tuplei{
    int x,y;
    //... the rest of methods and operand implementations
};

struct tupled{
    double x,y;
    //... the rest of methods and operand implementations
};

在我看来,当用户尝试使用某些方法实现中未考虑的类型时,第一种方法更容易更新和维护但不安全(这将需要过滤和路由到不同类型的实现,并且可能添加一些额外的操作),但是在第二种方法中,它会更安全,因为只接受特定类型但是用尽处理不同版本的代码来更新方法的实现,并且它是如此多余并涉及更多行代码。

期待从不同角度对此有所启发。

注意:我先搜索了一下,但在这件事上找不到什么

编辑:这里要考虑的另一点是,在第一种方法中包括我们想要使用类(cpp)的实现文件在使用使用泛型类型的成员方法时是不可避免的,但是在第二个我们可以只包括头文件(h)。似乎这会对主题[check this out]产生相关影响。

3 个答案:

答案 0 :(得分:3)

关于性能,这两种方式没有任何区别,因为泛型类型在编译期间被扩展,即编译器将生成与第二种方式相同的结构,但具有其他名称。

因为编译器会为您生成结构,所以二进制大小取决于您在代码中使用了多少种不同类型。如果您使用tuple<int>tuple<double>tuple<char>tuple<float>,则会生成四种不同的结构,这意味着与方法二相比,您的二进制文件会更大。但是你会发现你获得了灵活性并且维护也更容易(正如你已经说过的那样)。

如果您发现其中一个案例与其他案例有很大不同,那么将其分开或制作一个专门的模板,但总是假设您覆盖的不仅仅是三种类型,这样您就会发现维护更容易使用模板。

还有一件事是,由于模板的所有内容都是编译时间,因此不会出现运行时错误。即如果您将类型传递给模板,它将被编译并正常工作,或者编译器会给您一个错误。您没有得到正确编译代码并在运行时失败的情况。

答案 1 :(得分:3)

当然,二进制大小将依赖于编译器/链接器,但我还没有找到一种情况,使用类模板并生成适当的模板实例,实际上夸大了二进制大小,而不是手写的等效项,除非您的手写元组将通过dylib导出。

Linkers在消除多个翻译单元之间的冗余代码方面做得非常出色。这不是我认为理所当然的事情。在我之前的工作场所,我们不得不处理一个关于二进制分发大小的非常强​​烈的思维方式,并且必须有效地表明这些具有直接手写等效的类模板实际上并没有增加分发大小,而不是手写的等价物。 / p>

在某些情况下,任何类型的代码生成都会使二进制文件膨胀,但这通常适用于将代码生成用作动态分支形式(静态与动态多态性,例如)的静态替代方案的情况。例如,将std::sort与C qsort进行比较。如果你对一小部分可构造/可破坏的类型进行了分类,这些类型与std::sort然后qsort连续存储,那么qsort可能会产生一个较小的二进制文件,因为它不涉及代码生成而且只有代码生成每种类型所需的唯一代码是比较器。 std::sort会为每种类型生成一个全新的排序函数,并且可能会内联比较器。

也就是说,std::sort通常比qsort快2-3倍,以换取更大的二进制,因为交换动态调度用于静态调度,这通常是您看到代码生成有所不同的地方 - 当选择速度(使用代码生成)或更小的二进制大小(不使用)时。

有些美学可能会让你无论如何都喜欢手写版本:

struct tuplef{
    float x,y;
    //... the rest of methods and operand implementations
};

...但性能和二进制大小不应该在其中。如果您希望这些不同的元组在设计或实现中有更多分歧,那么这种方法会很有用。例如,您可能有tupled想要对齐其成员并使用SIMD和AoS代表,如此*:

*不是SIMD的一个很好的例子,它只受益于128位XMM寄存器,但希望足以说明问题。

struct tupled{
    ALIGN16 double xy[2];
    //... the rest of methods and operand implementations in SIMD
};

......如果你只有一个通用元组,这种变化可能会非常笨拙和笨拙。

template <class T>
struct tuple{
    T x,y;
    //... the rest of methods and operand implementations
};

值得注意的是,使用这样的类模板,您不一定需要将所有内容都设置为类的成员函数。您可以通过选择非会员来获得更多的灵活性和简单性:

typedef tuple<float> tuplef;
typedef tuple<double> tupled;

/// 'some_operation' is only available for floating-point tuples.
double some_operation(const tupled& xy) {...}
float some_operation(const tuplef& xy) {...}

...在some_operation的实现需要根据元组的类型相互分离的情况下,现在可以使用普通的旧函数重载。对于没有意义的类型,您也可以省略some_operation的重载,并获得您所讨论的那种过滤和路由行为。它还有助于防止tuple变成一个整体,以支持非成员,并将其与不适用于所有元组的操作分离。

当然,您也可以通过一些更高级的技术实现这一目标,同时仍然保持一切成为班级成员。然而,在这里支持非成员实现在各种类型的元组之间发生分歧,或仅适用于某些类型的元组的实现,可以帮助保持代码更加简单。您可以使用适用于所有元组的公分母操作的成员,并且以相同的方式实现,同时支持非成员进行元组类型之间不同的操作,例如。

答案 2 :(得分:1)

还有第三种选择。使用enable_if惯用法仅为特定类型启用模板。使用未启用的类型将导致编译器错误。实施例。

#include <type_traits>

// base template
template<typename T, typename = void>
struct tuple {};

template<typename T>
inline constexpr bool enable()
{ return std::is_integral<T>::value || std::is_floating_point<T>::value; }

// enable template for ints/floats, etc.
template<typename T>
struct tuple<T, typename std::enable_if<enable<T>()>::type>
{
    T x, y;
    // other methods here.
};

该方法结合了通用模板(即减少重复)的优点,同时还保持对使用类型的控制。请注意,可扩展性是有限的。如果用户想要引入新类型并使用tuple<T>,则他们将无法(当然没有提供重复的实现)。

编辑我意识到这个答案并没有直接回答你的问题,但我仍然认为它与这个话题有关,所以我把它作为一个社区帖子并保持原样。