仅将C ++编译器用于函数重载是不好的做法吗?


60

因此,我正在针对某个处理器使用C进行软件设计。该工具包包括编译C和C ++的功能。对于我正在做的事情,在这种环境下没有可用的动态内存分配,并且该程序总体上非常简单。更不用说该设备几乎没有处理器能力或资源。实际上,根本不需要使用任何C ++。

话虽这么说,我在一些地方进行函数重载(C ++的功能)。我需要发送几种不同类型的数据,并且不想使用printf带有某种%s(或其他任何一种)参数的样式格式。我见过有些人无法使用C ++编译器执行此操作printf,但就我而言,可以使用C ++支持。

现在,我确定可能会遇到一个问题,即为什么我需要重载一个函数。因此,我将尝试立即回答。我需要从串行端口传输不同类型的数据,所以我有一些重载,可以传输以下数据类型:

unsigned char*
const char*
unsigned char
const char

我只希望没有一种方法可以处理所有这些事情。当我呼吁我只是希望它传递出串行端口的功能,我没有很多资源的,所以我不想做几乎没有任何事情,但我的传送。

有人看到了我的程序,问我:“为什么要使用CPP文件?” 所以,这是我唯一的原因。那是不好的做法吗?

更新资料

我想回答一些问题:

解决您的困境的客观答案取决于:

  1. 如果使用C ++,可执行文件的大小是否会显着增加。

截至目前,可执行文件的大小占用了4.0%的程序内存(5248字节)和8.3%的数据内存(342字节)。也就是说,为C ++编译...我不知道对于C会是什么样子,因为我没有使用过C编译器。我确实知道该程序不会再增长了,所以对于资源的有限程度,我会说我还可以...

  1. 如果使用C ++,是否会对性能产生明显的负面影响。

好吧,如果有的话,我什么都没注意到...但是那又可能是为什么我问这个问题的原因,因为我不太了解。

  1. 代码是否可以在只有C编译器可用的其他平台上重用。

我知道,对此的答案肯定不是。实际上,我们正在考虑使用其他处理器,但仅考虑使用功能更强大的基于ARM的处理器(事实上,我所知道的所有处理器都具有C ++编译器工具链)。


59
众所周知,我仅在使用C功能的情况下将C ++用于一个项目,以便可以//发表评论。如果有效,为什么不呢?
Jules

76
不好的做法是,当您很好地利用了C语言所不提供的功能时,便只能使用C语言。
杰里·科芬

35
当您说“使用C ++编译器”时,您的意思是“使用C ++”。你说吧 您不能使用C ++编译器来编译C,但是可以轻松地从C切换到C ++。
user253751 '17

4
“对于我正在做的事情,处理器上没有动态内存分配,程序总体上相当简单。更不用说该设备几乎没有处理器能力或资源。实际上根本不需要任何C ++。” 我希望这两个句子中的第一个应该是不使用C ++的原因,因为如果这样的话,它们是非常糟糕的。C ++非常适合与嵌入式系统一起使用。
法拉普

21
@Jules我确定您知道这件事,并且想了一会儿,但如果有人不知道,则说明://自C99以来,注释已成为C标准。
戴维斯洛

Answers:


77

我不会走那么远,把它称为“坏习惯” 本身,但我也不是相信它真的给你的问题的解决方案。如果您只需要四个单独的函数来处理您的四种数据类型,那为什么C程序员自远古以来不做以下事情:

void transmit_uchar_buffer(unsigned char *buffer);
void transmit_char_buffer(char *buffer);
void transmit_uchar(unsigned char c);
void transmit_char(char c);

无论如何,这实际上就是C ++编译器在幕后所做的事情,对于程序员而言,这并没有那么大的开销。避免了所有“为什么用C ++编译器编写不十分C的问题”的问题,并且意味着您的项目中的其他任何人都不会被“允许” C ++的位和哪些位不允许的困惑。


8
我要说为什么不是因为要传输的类型是(可能是)实现细节,所以将其隐藏并让编译器处理选择实现可能会导致代码更具可读性。如果使用C ++功能可以提高可读性,那为什么不这样做呢?
Jules

29
一个人甚至可以#define使用C11_Generic
Deduplicator

16
@Jules,因为这对于允许在项目中使用哪些C ++功能非常困惑。包含对象的潜在更改是否会被拒绝?模板呢?C ++风格的注释?当然,您可以使用编码样式文档来解决此问题,但是如果您只是做函数重载的简单案例,那么只需编写C即可。
菲利普·肯德尔

25
@Phillip“ C ++样式注释”已经有效十多年了。
大卫·康拉德

12
@Jules:虽然传输的类型可能是制作保险报价的软件的实现细节,但是OPs应用程序听起来像是一个进行串行通信的嵌入式系统,其中类型和数据大小非常重要。
whatsisname

55

仅使用C ++的某些功能而不将其视为C并不完全常见,但也不是完全闻所未闻的。事实上,一些人甚至使用功能在所有C ++的,除了更严格和更强大的类型检查。他们只需编写C(注意只能在C ++和C的通用交集处编写),然后使用C ++编译器进行类型检查,并使用C编译器进行代码生成(或者始终坚持使用C ++编译器)。

Linux是一个代码库的示例,人们经常要求将诸如这样的标识符class重命名为klasskclass,以便他们可以使用C ++编译器来编译Linux。显然,考虑到Linus对C ++的看法它们总是会遭到拒绝:-D GCC是一个代码库的示例,该代码库首先转换为“ C ++-clean C”,然后逐渐重构为使用更多C ++功能。

您所做的没有错。如果您对C ++编译器的代码生成质量确实抱有偏执,可以使用Comeau C ++之类的编译器,将其编译为C作为目标语言,然后再使用C编译器。这样,您还可以对代码进行现场检查,以查看是否使用C ++注入了任何无法预料的对性能敏感的代码。但是,不应该只是重载,而是从字面上自动生成不同名称的函数– IOW,这恰好就是您在C语言中要做的事情。


15

解决您的困境的客观答案取决于:

  1. 如果使用C ++,可执行文件的大小是否会显着增加。
  2. 如果使用C ++,是否会对性能产生明显的负面影响。
  3. 代码是否可以在只有C编译器可用的其他平台上重用。

如果对任何一个问题的回答为“是”,则最好为不同的数据类型创建名称不同的函数并坚持使用C。

如果所有问题的答案都是“否”,那么我不认为您不应该使用C ++。


9
我必须说,我从未遇到过C ++编译器生成的代码比使用两种语言的共享子集编写的代码的C编译器要糟糕得多的代码。C ++编译器在大小和性能方面都表现不佳,但是我的经验是,使用C ++功能总是不适当的,从而导致了问题。尤其是,如果您担心大小,请不要使用iostream也不使用模板,但是您应该没问题。
Jules

@Jules:就其价值而言(IMO并不多),我已经看到了作为C和C ++的单个编译器(Turbo C ++ 1.0,如果有内存的话)出售的产品对于相同的输入会产生明显不同的结果。但是据我了解,这是在他们完成自己的C ++编译器之前,因此,即使看起来像一个外部编译器,它实际上也有两个完全独立的编译器-一个用于C,另一个用于C ++。
杰里·科芬

1
@JerryCoffin如果有记忆的话,Turbo产品从来就没有很高的声誉。而且如果它是1.0版本,您可能会因为没有得到高度完善而被原谅。因此,它可能不太具有代表性。
Barmar

10
@Barmar:实际上,他们在相当一段时间内确实享有相当不错的声誉。他们现在的声誉很差,主要是由于他们的纯粹年龄。它们与同一年份的其他编译器相比具有竞争力-但是没有人发布有关如何使用gcc 1.4(或其他功能)进行操作的问题。但是您是对的-它不是很有代表性。
杰里·科芬

1
@Jules我认为即使模板也很好。尽管有流行的理论,实例化模板并不会隐式地增加代码大小,但是除非使用了模板中的函数,否则代码大小通常不会增加(在这种情况下,大小增加将与函数大小成正比),或者除非模板是声明静态变量。内联规则仍然适用,因此可以在编译器内联所有函数的地方编写模板。
法拉普

13

您提出的问题似乎是使用C ++进行编译只是使您可以使用更多功能。不是这种情况。使用C ++编译器进行编译意味着您的源代码被解释为C ++源代码,并且C ++是与C语言不同的语言。两者的公共子集足够大,可以有效地进行编程,但是每个子集都具有另一个缺少的功能,并且可以编写两种语言都可接受但解释不同的代码。

如果您真正想要的只是函数重载,那么我真的看不出将C ++引入其中来混淆问题的意义。可以区分它们的参数列表,而不是使用相同名称的不同函数,而只需编写具有不同名称的函数即可。

至于你的客观标准

  1. 如果使用C ++,可执行文件的大小是否会显着增加。

相对于类似的C代码,可执行文件在编译为C ++时可能会稍大。至少,C ++可执行文件对于所有函数名称都具有C ++名称处理的可疑优势,以至于任何符号都保留在可执行文件中。(实际上,这实际上是为您提供过载的原因。)您必须通过实验确定差异是否足够大以至于对您来说不重要。

  1. 如果使用C ++,是否会对性能产生明显的负面影响。

我怀疑您所描述的代码与假设的纯C模拟代码是否会看到明显的性能差异。

  1. 代码是否可以在只有C编译器可用的其他平台上重用。

我对此稍有不同:如果您想将使用C ++构建的代码链接到其他代码,那么其他代码也需要用C ++编写并使用C ++构建,或者您需要在您自己的代码中进行特殊设置(声明为“ C”链接),此外,对于重载的函数,您根本无法做这件事。另一方面,如果您的代码是用C编写并编译为C的,则其他人可以将其与C和C ++模块链接。通常可以通过翻转表格来解决此类问题,但是既然您似乎真的不需要C ++,那么为什么首先要接受这样的问题呢?


4
“当编译为C ++时,可执行文件可能会稍大...以至于任何符号都保留在可执行文件中。” 公平地说,任何体面的优化工具链都应具有链接器选项,以在“发行”版本中剥离这些符号,这意味着它们只会使调试版本膨胀。我不认为这是一个重大损失。实际上,这通常是一种好处。
科迪·格雷

5

过去,提倡将C ++编译器用作“更好的C” 作为用例。实际上,早期的C ++就是这样。基本的设计原则是,您只能使用所需的功能,而不会产生不使用的功能的成本。因此,您可以重载函数(通过使用overload关键字声明意图!),而项目的其余部分不仅可以很好地编译,而且生成的代码不会比C编译器所产生的差。

从那时起,语言有所不同。

mallocC代码中的每个代码都会出现类型不匹配错误-在没有动态内存的情况下,这不是问题!同样,voidC语言中的所有指针都会使您绊倒,因为您将不得不添加显式强制转换。可是......你为什么做这样 ......你会带领下来使用C ++功能越来越多的路径。

因此,可能需要做一些额外的工作。但是它将用作大规模采用C ++的门户。几年后,人们会抱怨您的旧代码看起来像是1989年编写的,因为他们将您的malloc调用替换为new,撕下循环体的代码块以仅调用算法,并谴责这种不安全的假多态性如果允许编译器执行此操作,则微不足道。

另一方面,您知道用C编写它会完全一样,因此鉴于C的存在,用C而不是C ++编写是否总是错误的?如果答案是“否”,那么使用C ++精心挑选的功能也不会错


2

许多编程语言具有围绕它们发展的一种或多种文化,并且对一种语言应该“成为”什么有特定的想法。虽然有一种低级语言适合系统编程应该是可行,实用和有用的,它使用C ++的某些功能来扩展适合于此目的的C语言,这两种语言同样适用,但围绕这两种语言的文化并不适用。特别喜欢这种合并。

我读过一个嵌入式C编译器,该编译器结合了C ++的一些功能,包括具有

foo.someFunction(a,b,c);

本质上解释为

typeOfFoo_someFunction(foo, a, b, c);

以及重载静态函数的能力(名称重整问题仅在导出时出现函数已重载)。没有理由在不对链接和运行时环境施加任何其他要求的情况下C编译器不应该支持这种功能,但是许多C编译器为了避免环境负担而反身拒绝了C ++的几乎所有内容,从而导致它们甚至不接受不会增加此类成本的功能。同时,C ++文化对无法支持该语言所有功能的任何环境都没有兴趣。除非或直到C的文化改变为接受此类功能,或者C的文化改变以鼓励轻量子集的实现,否则“混合”代码易于遭受两种语言的许多限制,同时其获取能力受到限制。在组合超级集中操作的好处。

如果平台同时支持C和C ++,则支持C ++所需的额外库代码可能会使可执行文件更大一些,尽管效果不会特别明显。C ++中“ C功能”的执行可能不会受到特别的影响。更大的问题可能是C和C ++优化器如何处理各种极端情况。C中数据类型的行为主要由存储在其中的位的内容定义,并且C ++具有公认的结构类别(PODS-普通旧数据结构),其语义同样主要由存储的位定义。但是,C和C ++标准有时都允许与存储的位所暗示的行为相反的行为,并且两种语言之间“存储的位”行为的例外有所不同。


1
有趣的意见,但未解决OP的问题。
R Sahu

@RSahu:与不适合某种语言的“文化”相适应的代码易于获得更好的支持,并且更容易适应各种实现。C和C ++的文化都不符合OP所建议的用法,我将针对具体问题添加一些更具体的观点。
超级猫

1
请注意,C ++标准委员会中有一个嵌入式/财务/游戏小组,旨在准确解决您声称被丢弃的“小额利益”情况。
Yakk

@Yakk:我承认我并没有非常密切地关注C ++世界,因为我的兴趣主要在于C。我知道已经在努力开发更多的嵌入式方言,但是我还没有想到它们真的可以普及到任何地方。
超级猫

2

要考虑的另一个重要因素:谁将继承您的代码。

那个人总是会成为拥有C编译器的C程序员吗?我想是这样。

那个人还会是拥有C ++编译器的C ++程序员吗?在使我的代码依赖于C ++特定事物之前,我希望对此有一定的把握。


2

多态是C ++免费提供的一个非常好的功能。但是,在选择编译器时,您可能还需要考虑其他方面。C语言中有一种多态性的替代方法,但使用它会引起一些眉毛。其可变参数功能,请参阅可变参数功能教程

你的情况是

enum serialDataTypes
{
 INT =0,
 INT_P =1,
 ....
 }Arg_type_t;
....
....
void transmit(Arg_type_t serial_data_type, ...)
{
  va_list args;
  va_start(args, serial_data_type);

  switch(serial_data_type)
  {
    case INT: 
    //Send integer
    break;

    case INT_P:
    //Send data at integer pointer
    break;
    ...
   }
 va_end(args);
}

我喜欢Phillips的方法,但是它给您的图书馆带来了很多麻烦。上面的界面干净。它确实有其缺点,并且最终是一个选择问题。


可变参数函数主要用于参数数量类型均可变的用例。否则,您将不需要它,尤其是对于您的特定示例而言,它是过大的选择。使用接受数据的an Arg_type_tvoid *指向数据的普通函数会更简单。话虽这么说,是的,给(单个)函数一个表示数据类型的参数确实是一个可行的选择。
John Bollinger

1

对于特定静态地为几种不同类型重载“函数”的特殊情况,您可以考虑使用C11及其_Generic机制。我觉得这可能足以满足您的有限需求。

使用Philip Kendall的答案,您可以定义:

#define transmit(X) _Generic((X),      \
   unsigned char*: transmit_uchar_buffer, \
   char*: transmit_char_buffer,           \
   unsigned char: transmit_uchar,         \
   char: transmit_char) ((X))

并编写transmit(foo) 任何类型的代码foo(在上面列出的四种类型中)。

如果您只关心GCC(和兼容的,例如Clang)编译器,则可以考虑__builtin_types_compatible_ptypeof扩展。


1

仅将C ++编译器用于函数重载是不好的做法吗?

恕我直言的观点,是的,由于我喜欢这两种语言,因此我需要变得精神分裂以回答这一问题,但这与效率无关,而更像是安全性和惯用语言。

C面

从C的角度来看,让您的代码仅使用C函数重载就要求C ++非常浪费。除非您将其用于带有C ++模板的静态多态性,否则它是获得了如此琐碎的语法糖,以换成完全不同的语言。此外,如果您想将函数导出到dylib(可能或可能不切实际),那么对于所有带有名称纠缠符号的广泛使用而言,您将无法再进行实际操作。

C ++面

从C ++的角度来看,您不应该使用带有函数重载的C ++之类的C ++。这不是风格上的教条主义,而是与日常C ++的实际使用有关的一种。

如果您使用的C类型系统禁止复制诸如ctor之类的代码,那么您通常的C代码就只能做到合理合理且“安全”。 structs。一旦您在C ++更为丰富的类型系统中工作,那么日常功能就变得非常有价值,memsetmemcpy这些功能不会变成日常功能,您应该一直依靠它们。取而代之的是,它们通常像瘟疫一样避免使用,因为对于C ++类型,您不应将它们像原始位和字节那样对待,以进行复制和改组并释放。即使您的代码目前仅memset在原语和POD UDT上使用诸如此类的东西,此刻任何人都将ctor添加到您使用的任何UDT中(包括仅添加需要一个成员的成员,例如std::unique_ptr成员)针对此类函数或虚函数或任何此类函数,它将使您的所有常规C风格编码容易受到未定义行为的影响。从赫伯·萨特本人那里得到:

memcpymemcmp违反类型系统。使用memcpy复制的对象是像使用复印机赚钱。使用memcmp比较对象就像通过计算豹子的斑点来比较它们。这些工具和方法似乎可以完成任务,但是它们太粗糙了,无法接受。C ++对象都是关于信息隐藏的(可以说是软件工程中最有利可图的原理;请参阅第11项):对象可以隐藏数据(请参阅第41项),并设计精确的抽象,以便通过构造函数和赋值运算符复制数据(请参阅第52到55项)。 。推销所有这些memcpy都是严重违反信息隐藏的行为,并且通常会导致内存和资源泄漏(充其量),崩溃(更糟)或未定义的行为(最糟糕)-C ++编码标准。

如此之多的C开发人员都会对此表示反对,这是正确的,因为这种哲学在您使用C ++编写代码时才适用。如果您像在C ++那样构建的代码中一直使用函数,那么很可能编写出非常有问题的代码,但是如果您使用C来编写代码,那将是非常好的事情memcpy。由于类型系统的差异,两种语言在这方面有很大的不同。看看这两个共同点的功能子集是很诱人的,并且相信它们可以像另一个一样使用,特别是在C ++方面,但是C +代码(或C--代码)通常比C和C ++代码。

同样,malloc如果它可以直接调用可以抛出的任何C ++函数,则不应在C风格的上下文中使用它(这不意味着没有EH),因为那样的话,您的函数中就会隐含退出点在能够free存储该内存之前,您不能有效地捕获编写C样式代码的异常。所以,只要你有基础,因为C ++有一个文件.cpp扩展名或任何和它所有这些类型的东西一样mallocmemcpymemsetqsort,等等,如果不是这样,那么它会进一步提出问题,除非它是仅适用于原始类型的类的实现细节,此时它仍需要进行异常处理以确保异常安全。如果你正在编写C ++代码的你,而不是想通常依靠RAII和使用之类的东西vectorunique_ptrshared_ptr,等,并尽可能避免一切正常的C风格的编码。

您可以使用C和X射线数据类型的剃须刀并使用其位和字节进行播放而不会造成团队附带损害的原因(尽管您仍然可以通过两种方式伤害自己)并不是因为C类型可以做到,但是由于它们永远无法做到。在将C的类型系统扩展为包括ctor,dtor和vtables之类的C ++功能以及异常处理的那一刻,所有惯用的C代码都将被呈现,远比当前危险得多,并且您将看到一种新的理念和思维方式的发展将鼓励完全不同的编码风格,正如您在C ++中所看到的那样,它现在考虑甚至对管理内存的类甚至使用与RAII兼容的资源(例如,unique_ptr。这种心态并不是出于绝对的安全感。它是C ++专门针对某些异常情况(例如仅允许的情况)进行安全处理而演变而来的其在其类型系统中类的功能而开发的。

例外安全

同样,当您进入C ++领域时,人们会期望您的代码是异常安全的。人们可能会在将来维护您的代码,因为该代码已经用C ++编写并编译,并且只需std::vector, dynamic_cast, unique_ptr, shared_ptr在代码直接或间接调用的代码中使用等等,就可以认为它是无害的,因为您的代码已经“应该”为C ++码。在这一点上,我们必须面对可能抛出的错误的机会,然后当您使用完全精美的C代码时,如下所示:

int some_func(int n, ...)
{
    int* x = calloc(n, sizeof(int));
    if (x)
    {
        f(n, x); // some function which, now being a C++ function, may 
                 // throw today or in the future.
        ...
        free(x);
        return success;
    }
    return fail;
}

...它现在坏了。需要重写它以确保异常安全:

int some_func(int n, ...)
{
    int* x = calloc(n, sizeof(int));
    if (x)
    {
        try
        {
            f(n, x); // some function which, now being a C++ function, may 
                     // throw today or in the future (maybe someone used
                     // std::vector inside of it).
        }
        catch (...)
        {
            free(x);
            throw;
        }
        ...
        free(x);
        return success;
    }
    return fail;
}

毛!这就是为什么大多数C ++开发人员会要求这样做的原因:

void some_func(int n, ...)
{
    vector<int> x(n);
    f(x); // some function which, now being a C++ function, may throw today
          // or in the future.
}

上面是C ++开发人员通常会批准的符合RAII的异常安全代码,因为该函数不会泄漏没有哪一行代码会因以下原因触发隐式退出 throw

选择一种语言

您应该使用RAII,异常安全性,模板,OOP等拥抱C ++的类型系统和原理,或者拥抱C很大程度上围绕原始位和字节进行旋转。您不应该在这两种语言之间形成邪恶的婚姻,而应将它们分成不同的语言以区别对待,而不是将它们混淆在一起。

这些语言想嫁给你。通常,您必须选择一个,而不要同时约会和鬼混。或者,您也可以像我一样成为一夫多妻主义者,并且都可以结婚,但是在彼此相处时,您必须彻底改变自己的想法,并使他们彼此分开,以免彼此争斗。

二进制大小

出于好奇,我现在尝试使用我的免费列表实现和基准测试,并将其移植到C ++,因为我对此感到非常好奇:

[...]不知道C语言会是什么样子,因为我没有使用过C编译器。

...并且想知道二进制大小是否只会以C ++的形式膨胀。它要求我在所有地方都进行显式转换(这很糟糕(我更喜欢用C更好地编写诸如分配器和数据结构之类的底层代码)的一个原因),但只花了一分钟。

这只是将一个简单的控制台应用程序的MSVC 64位版本构建与不使用任何C ++功能的代码进行了比较,甚至没有运算符重载-只是使用C进行构建与使用(<cstdlib>而不是<stdlib.h>和)之间的区别。诸如此类的事情,但令我惊讶的是它对二进制大小的影响为零!

二进制文件9,728在C语言中构建9,278时为字节,在编译为C ++代码时同样为字节。我其实没想到。我以为像EH之类的东西至少会在那儿增加一点(认为至少会相差一百个字节),尽管大概它能够确定出不需要添加与EH相关的指令,因为我只是使用C标准库,没有任何异常。我在想什么就像RTTI一样,都会增加二进制大小。无论如何,看到它真是太酷了。当然,我不认为您应该从这一结果中概括一下,但这至少让我印象深刻。它也不会对基准产生任何影响,自然也是如此,因为我认为相同的结果二进制大小也意味着相同的结果机器指令。

也就是说,谁在乎上述安全性和工程问题的二进制大小?因此,再次选择一种语言并拥护其哲学,而不要试图将其混为一谈。这就是我的建议。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.