2

我正要为一个项目创建一个词法分析器,证明它的概念存在,这个想法是可行的等等,我正要开始写它,我意识到:

为什么是字符?

(我正在远离 C,我仍然对标准库持怀疑态度,我觉得处理 char* 用于偏移等比了解字符串更容易)

为什么不使用 w_char 或其他东西、整数或任何类型(假设它有一些已定义的操作)。

那么我应该使用模板吗?到目前为止,我似乎应该是的,但我可以考虑两个相反的论点:

首先,模块化的复杂性,当我编写“模板”时,它必须放在头文件中/可以用于任何使用它的实现(这不是隐藏源代码的问题,我不介意必须显示代码部分,它将是免费的(如自由)软件)这意味着额外的解析和类似的事情。

我的C背景尖叫着不要这样做,我似乎想要单独的.o文件,我明白为什么我不能顺便说一句,我不是在寻求方法。单独的目标文件会加速复杂化,因为 make 文件(你告诉它或让它使用 -MM 和编译器自行确定)不会为没有改变的东西运行复杂化命令等等。

其次,对于模板,我知道没有办法指定类型必须做什么,除了让用户意识到什么时候失败(你知道 Java 如何有一个 extends 关键字)我怀疑 C++11 建立在此之上,作为元-programming 是《C++ 编程语言》第 4 版中的一个大章节。

这些天来这些理由重要吗?我学到了以下内容:“您不是创建一个可以编译的巨大代码文件,而是创建链接的小文件”,而模板似乎与此背道而驰。

我确信 G++ 解析速度非常快,但它也进行了优化,如果它花费大量时间优化一个文件,它会在每次在翻译单元中看到它时重新进行优化,与单独的目标文件一样,它只做了一次(一般优化),如果您使用 LTO(链接时间优化),可能会做更多

或者我可以创建一个类,词法分析器的每个输入都来自并使用它(我相信它被称为通用编程),但我的 C 根说“eww virtuals”并敦促我使用 char*

我知道这是非常开放的,我只是不知道在使用模板和不使用模板之间划清界限。

4

3 回答 3

4

模板不必在标题中!如果您只有几个实例化,您可以在合适的翻译单元中显式实例化类和函数模板。也就是说,模板将分为三个部分:

  1. 声明模板的标头。
  2. 包含第一个和实现模板的标题,但仅包含在第三组文件中。
  3. 源文件,包括 2. 中的标头,并显式实例化具有相应类型的模板。

这些模板的用户只会包含标头,而不会包含实现标头。可以做到这一点的一个例子是 IOStreams:基本上只有两个实例化:一个 forchar和一个 for wchar_t。是的,您可以实例化其他类型的流,但我怀疑有人会这样做(我有时会质疑是否有人使用具有不同字符类型的流,char但可能是人们)。

也就是说,模板使用的概念确实没有在源代码中明确表示,C++11 也没有添加任何工具来这样做。有关于向 C++ 添加概念的讨论,但到目前为止它们还不是任何标准的一部分。我认为有一个概念轻量级提案将包含在 C++14 中。

但是,在实践中,我并没有发现太大的问题:很有可能记录概念并使用诸如static_assert()潜在地产生更好的错误消息之类的东西。问题在于许多概念实际上比底层算法更具限制性,而且额外的松弛有时非常有用。

这是一个关于如何实现和实例化模板的简短且有些虚构的示例。这个想法是实现类似std::basic_ostream但仅提供字符串输出运算符的缩小版本:

// simple-ostream.hpp
#include "simple-streambuf.hpp"
template <typename CT>
class simple_ostream {
     simple_streambuf<CT>* d_sbuf;
public:
     simple_ostream(simple_streambuf<CT>* sbuf);
     simple_streambuf<CT>* rdbuf() { return this->d_sbuf; } // should be inline
};
template <typename CT>
simple_ostream<CT>& operator<< (simple_ostream<CT>&, CT const*);

除了rdbuf()成员之外,上面只是一个带有几个成员声明和一个函数声明的类定义。该rdbuf()功能是直接实现的,以表明您可以将需要性能的可见实现与解耦更重要的外部实现混合和匹配。使用的类模板simple_streambuf被认为类似于std::basic_streambuf并且至少在 header 中声明"simple-streambuf.hpp"

// simple-ostream.tpp
// the implementation, only included to create explicit instantiations
#include "simple-ostream.hpp"

template <typename CT>
simple_ostream<CT>::simple_ostream(simple_streambuf<CT>* sbuf): d_sbuf(sbuf) {}

template <typename CT>
simple_ostream<CT>& operator<< (simple_ostream<CT>& out, CT const* str) {
    for (; *str; ++str) {
        out.rdbuf()->sputc(*str);
    }
    return out;
}

仅在显式实例化类和函数模板时才包含此实现标头。例如,for 的实例化char如下所示:

// simple-ostream-char.cpp
#include "simple-ostream.tpp"

// instantiate all class members for simple_ostream<char>:
template class simple_ostream<char>;
// instantiate the free-standing operator
template simple_ostream<char>& operator<< <char>(simple_ostream<char>&, char const*);

的任何使用simple_ostream<CT>都只会包括simple-ostream.hpp. 例如:

// use-simple-ostream.cpp
#include "simple-ostream.hpp"

int main()
{
    simple_streambuf<char> sbuf;
    simple_ostream<char>   out(&sbuf);
    out << "hello, world\n";
}

当然,要构建可执行文件,您将需要两者use-simple-ostream.osimple-ostream-char.o但假设模板实例化是库的一部分,这并没有真正增加任何复杂性。唯一真正令人头疼的是,当用户想要使用具有意外实例化的类模板时,例如char16_t,但只提供了charwchar_t:在这种情况下,用户需要显式创建实例化,或者,如果需要,包括实现头文件。

如果你想尝试这个例子,下面是一个有点简单和草率的(因为只是标题)的实现simple-streambuf<CT>

#ifndef INCLUDED_SIMPLE_STREAMBUF
#define INCLUDED_SIMPLE_STREAMBUF
#include <iostream>

template <typename CT> struct stream;
template <>
struct stream<char> {
    static std::ostream& get() { return std::cout; }
};
template <>
struct stream<wchar_t> {
    static std::wostream& get() { return std::wcout; }
};

template <typename CT>
struct simple_streambuf
{
    void sputc(CT c) {
        stream<CT>::get().rdbuf()->sputc(c);
    }
};

#endif
于 2013-08-28T00:40:37.770 回答
2

是的,它应该仅限于字符。为什么 ?因为你问...

我对模板的经验很少,但是当我使用模板时,这种必要性自然而然地出现了,我不需要尝试使用模板。

我的 2 美分,FWIW。

于 2013-08-28T00:36:44.193 回答
0

1:首先,模块化的复杂化,我写“模板”的那一刻,它必须放在头文件中……</p>

这不是一个真正的论点。您可以使用 C、C++、结构、类、模板、具有虚函数的类以及多范式语言的所有其他好处。您不会被迫对您的设计采取全有或全无的方法,您可以根据您的设计需求混合和匹配这些功能。因此,您可以在它们是适当工具的地方使用模板,在模板不理想的地方使用其他构造。很难知道那会是什么时候,直到你有使用它们的经验之后。仅模板/标头库很受欢迎,但使用该方法的原因之一是它们简化了链接和构建过程,并且如果设计得当可以减少依赖关系。如果它们设计得不好,那么是的,它们会导致编译时间激增。那不是语言'

哎呀,您甚至可以将您的实现放在 C 不透明类型之后,并为所有内容使用模板,从而使核心模板代码仅对一个翻译可见。

2:其次,对于模板,我无法指定类型必须做什么......</p>

这通常被认为是一个特征。实例化可以导致进一步的实例化,这些实例化能够实例化不同的实现和专业化——这是模板元编程领域。通常,您真正需要做的就是实例化实现,从而评估类型和参数。然而,这——“概念”的模拟和接口验证——会增加你的构建时间。但此外,这可能不是最好的设计,因为在许多情况下推迟实例化更可取。

如果你只需要暴力实例化你的所有变体,一种方法是创建一个单独的翻译来做到这一点——你甚至不需要将它链接到你的库;将其添加到单元测试或某个单独的目标中。这样,您可以验证实例化和功能是否正确,而不会对您的客户(包括/链接到库)产生重大影响。

这些天来这些理由重要吗?

不。构建时间当然非常重要,但我认为您只需要学习正确的工具来使用,以及当/如果您需要快速构建和可扩展性时必须抽象某些实现(或放在编译防火墙之后)的时间和原因大型项目。所以是的,它们很重要,但是好的设计可以在多功能性和构建时间之间取得良好的平衡。还要记住,模板元编程能够将大量程序验证从运行时转移到编译时。所以对编译时间的打击不一定是坏事,因为它可以让你避免很多运行时验证/问题。

我确信 G++ 解析非常快,但它也进行了优化,如果它花费大量时间优化一个文件,它会在每次在翻译单元中看到它时重新进行优化......</p>

正确的; 这种冗余会扼杀快速的构建时间。

与单独的目标文件一样,它只做了一次(一般优化),如果你使用 LTO(链接时间优化),可能会更多......单独的目标文件加速了复杂性,因为 make 文件(你告诉它或有它与编译器一起使用 -MM 来自行计算)不会对未更改的事物运行复杂命令等等。

不一定如此。首先,许多目标文件对链接器产生大量需求。其次,它增加了工作量,因为你有更多的翻译,所以减少目标文件是一件好事。这实际上取决于您的库和依赖项的结构。一些团队采取相反的方法(我经常这样做),并使用产生很少目标文件的方法。这可以使复杂项目的构建速度提高很多倍,因为您消除了编译器和链接器的冗余工作。为获得最佳结果,您需要充分了解流程和依赖关系。在大型项目中,减少翻译/对象可以使构建速度提高很多倍。这通常被称为“Unity Build”。

所以简短的回答是:使用最好的工具来解决手头的问题——一个好的设计师会使用许多可用的工具。您还远远没有耗尽工具和构建系统的功能。对这些主题的良好理解将需要数年时间。

于 2013-08-28T02:05:18.963 回答