在coderbyte网站上,'gets(stdin)'发生了什么?

145

Coderbyte 是一个在线编程挑战网站(我刚刚发现它)。

你将遇到的第一个 C++ 挑战需要修改给出的 C++ 初始代码:

#include <iostream>
#include <string>
using namespace std;

int FirstFactorial(int num) {

  // Code goes here
  return num;

}

int main() {

  // Keep this function call here
  cout << FirstFactorial(gets(stdin));
  return 0;

}

如果你对C ++略有了解,第一件引起注意的事情是:

int FirstFactorial(int num);
cout << FirstFactorial(gets(stdin));

所以,好的,该代码调用了gets,自C++11起就被弃用,自C++14起就被移除了,这本身就很糟糕。

但后来我意识到:gets的类型是char*(char*)。它不应该接受一个FILE*参数,并且结果也不应该在int参数的位置使用,但是……不仅它可以编译而且可以正常运行并将正确的输入值传递给FirstFactorial

在该特定网站之外,该代码无法编译(如预期),那么这里发生了什么?


*实际上第一个问题是using namespace std,但这与我的问题无关。


请注意,标准库中的stdin是一个FILE*,而任何类型的指针都会转换为char*,这是gets()函数参数的类型。然而,在混淆C代码比赛之外,您永远不应该编写那种代码。如果您的编译器甚至接受它,请添加更多警告标志;如果您正在尝试修复具有该结构的代码库,请将警告转换为错误。 - Davislor
1
@Davislor 不是的,它显示了“候选函数不可行:第一个参数没有已知的从'struct _IO_FILE *'到'char *'的转换”。 - bolov
3
@Davislor 嗯,这可能适用于古老的C语言,但绝对不适用于C++。 - Quentin
@Quentin 是的,这是不应该编译的。原本的挑战可能是“修复这个有问题的代码,并能理解它的意图”,但如果是这样,就需要提供一个真正的规范和测试用例。 - Davislor
6
我很惊讶没有人尝试过这个,但 gets(stdin)(多了一个空格)会产生预期的 C++ 错误。 - Roman Odaisky
@RomanOdaisky 我们已经确定这是一个文本查找和替换,所以没有什么意外的。 - bolov
3个回答

174

我是Coderbyte的创始人,也是创建gets(stdin)这个技巧的人。

评论区的留言正确,这是一种查找和替换的形式,所以让我很快地解释一下为什么我要这样做。

早在我创建该网站的那天(大约在2012年左右),它只支持JavaScript。在运行在浏览器中的JavaScript中没有办法“读入输入”,因此会有一个函数foo(input),我使用Node.js中的readline()函数调用它,如foo(readline())。但是当时我还只是一个孩子,并不知道更好的方法,所以在运行时我直接用输入替换了readline()。因此,foo(readline())变成了foo(2)foo("hello"),这在JavaScript中运行得很好。

在2013/2014左右,我添加了更多的语言并使用第三方服务来在线评估代码,但是对于使用的服务来说,使用stdin/stdout非常困难,所以对于Python、Ruby,以及最终的C++、C#等语言,我仍然坚持使用同样愚蠢的查找和替换方式。

快进到今天,我在自己的容器中运行代码,但从未更新过stdin/stdout的工作方式,因为人们已经习惯了这种奇怪的技巧(有些人甚至在论坛上发布帖子解释如何绕过它)。

我知道这不是最佳实践,对于正在学习新语言的人来说看到这样的hack并没有什么帮助,但我的想法是让新编程者不用担心读取输入,专注于编写算法来解决问题。几年前,关于编码挑战网站的一个常见抱怨是新编程者会花很多时间弄清楚如何从stdin读取或读取文件中的行,因此我希望新编程者能够在Coderbyte上避免这个问题。

我将很快更新整个编辑器页面以及默认代码和语言的stdin读取。希望到时候C++程序员们能更享受使用Coderbyte :)


22
“但是这个想法是为了让新程序员不必担心读取输入,而只专注于编写算法来解决问题”,你没想过在那个位置上写一个编造的函数名或明显的占位符,而不是编写类似于“真实”代码的东西吗?我真的很好奇。 - Ruther Rendommeleigh
25
当我发帖时,我真的没想到我会选择除自己之外的答案。感谢您以这样出色的方式证明了我的错误。看到您的回答真是一种愉悦。 - bolov
4
非常有趣!如果你想使用这个技巧,我建议你将函数调用替换为类似于 TAKE_INPUT 的东西,然后使用查找和替换将 #define TAKE_INPUT whatever_here 插入到文件的开头。请注意不要改变原来的意思。 - Draconis
18
我们需要更多以_"我是X的创始人,也是创造这个的那个人"_开头的回答。 - pipe
2
@iheanyi 没有人要求它完美。事实上,我相信几乎任何占位符都比像有效代码一样看起来但实际上不能编译的东西更好,尤其是对于新手来说。 - Ruther Rendommeleigh
显示剩余6条评论

113

我很感兴趣。现在是时候戴上调查眼镜了,由于我没有访问编译器或编译标志的权限,所以我需要发挥创意。此外,由于这段代码毫无意义,质疑每个假设都不是一个坏主意。

首先,让我们检查gets的实际类型。我有一个小技巧:

template <class> struct Name;

int main() { 
    
    Name<decltype(gets)> n;
  
  // keep this function call here
  cout << FirstFactorial(gets(stdin));
  return 0;
    
}

这看起来很正常:

/tmp/613814454/Main.cpp:16:19: warning: 'gets' is deprecated [-Wdeprecated-declarations]
    Name<decltype(gets)> n;
                  ^
/usr/include/stdio.h:638:37: note: 'gets' has been explicitly marked deprecated here
extern char *gets (char *__s) __wur __attribute_deprecated__;
                                    ^
/usr/include/x86_64-linux-gnu/sys/cdefs.h:254:51: note: expanded from macro '__attribute_deprecated__'
# define __attribute_deprecated__ __attribute__ ((__deprecated__))
                                                  ^
/tmp/613814454/Main.cpp:16:26: error: implicit instantiation of undefined template 'Name<char *(char *)>'
    Name<decltype(gets)> n;
                         ^
/tmp/613814454/Main.cpp:12:25: note: template is declared here
template <class> struct Name;
                        ^
1 warning and 1 error generated.

gets 标记为已弃用,其签名为 char *(char *)。但是,FirstFactorial(gets(stdin)); 怎么会编译通过呢?

让我们尝试其他方法:

int main() { 
  Name<decltype(gets(stdin))> n;
  
  // keep this function call here
  cout << FirstFactorial(gets(stdin));
  return 0;
    
} 

这给我们带来了:
/tmp/286775780/Main.cpp:15:21: error: implicit instantiation of undefined template 'Name<int>'
  Name<decltype(8)> n;
                    ^

终于有新进展了:decltype(8)。因此整个gets(stdin)都被文本替换为输入(8)。

事情变得更加奇怪。编译器错误仍然存在:

/tmp/596773533/Main.cpp:18:26: error: no matching function for call to 'gets'
  cout << FirstFactorial(gets(stdin));
                         ^~~~
/usr/include/stdio.h:638:14: note: candidate function not viable: no known conversion from 'struct _IO_FILE *' to 'char *' for 1st argument
extern char *gets (char *__s) __wur __attribute_deprecated__;

现在我们对cout << FirstFactorial(gets(stdin));得到了预期的错误。

我检查了宏,因为#undef gets似乎没有任何作用,所以看起来它不是宏。

但是

std::integral_constant<int, gets(stdin)> n;

它编译了。

但是

std::integral_constant<int, gets(stdin)> n;    // OK
std::integral_constant<int, gets(stdin)> n2;   // ERROR                                          wtf??

在代码的n2行没有预期的错误。

而且,对main的几乎任何修改都会使cout << FirstFactorial(gets(stdin));这一行输出预期的错误信息。

此外,实际上stdin是空的。

因此,我只能推断和猜测他们有一个小程序,解析源代码并试图(很差地)用测试用例输入值替换gets(stdin),然后再将其馈送到编译器。如果有人有更好的理论或确切知道他们在做什么,请分享!

这显然是一种非常不好的做法。在研究这个问题时,我发现至少有一个问题在这里(example),因为人们不知道有一个网站会这样做,所以他们的答案是“不要使用gets,使用...代替”,这确实是一个好建议,但只会让OP更加困惑,因为在这个网站上任何尝试从stdin中读取有效数据的操作都会失败。


太长不看

gets(stdin)在C++中是无效的。这是这个特定网站使用的噱头(我不是支持也不是反对它)。如果你想继续在这个网站上提交代码,你必须使用这个看起来没有意义的结构,但要注意它很脆弱。对main的几乎任何修改都会导致错误。在这个网站之外,请使用常规的输入方法。


28
我真的很惊讶。也许这个问答可以成为一个经典帖子,阐述为什么不要从编程挑战网站学习编程。 - alter_igel
28
有些非常邪恶的事情正在发生,我认为它涉及到编译器之外的源代码文本替换层面。试一下这个:std::cout << "gets(stdin)";,输出结果是 8(或者您在“输入”字段中输入的任何内容)。这是对语言的可耻滥用。 - alter_igel
14
注意"gets(stdin)"周围的引号。这是一个字符串字面值,即使预处理器也不会触及它。 - alter_igel
2
引用詹姆斯·柯克的话:“这真是奇怪至极。” - ApproachingDarknessFish
2
@alterigel,请别趾高气昂。这并不意味着从编程挑战网站学习是否有用。你有什么资格决定人们如何练习技能? - Matsemann
显示剩余6条评论

65

我在Coderbyte编辑器中尝试了以下对main的添加:

std::cout << "gets(stdin)";

在一个字符串字面量中出现神秘而难以捉摸的片段gets(stdin)。这不应该被任何东西转换,甚至不是预处理器,任何 C++程序员都应该期望这段代码打印出精确的字符串gets(stdin)到标准输出。但当编译并在coderbyte上运行时,我们看到以下输出:

8

这里的值8直接从编辑器下方方便的“输入”字段中获取。

Magic code

由此可见,该在线编辑器正在对源代码执行盲目的查找和替换操作,使用者的“输入”用作替换gets(stdin)。我个人认为这是一种比粗心的预处理宏更糟糕的语言误用。

在在线编码挑战网站的背景下,我对此感到担忧,因为它教授了非常规、非标准、毫无意义,至少不安全的实践方法,例如gets(stdin),而且无法在其他平台上重复。

我相信,仅仅使用std::cin并将输入流到程序中并不会很难。


甚至不是盲目的“查找和替换”,因为有时它会替换,有时则不会。 - bolov
4
@bolov 可能仅仅替换了第一次出现的 gets(stdin) 吗?我的意思是 'blind' 是指它似乎不知道语言的语法或文法。 - alter_igel
是的,你说得对。它替换了第一次出现的内容。我尝试在 main 函数之前放置一个,确实得到了这个结果。 - bolov
1
进一步的研究表明,该网站不仅适用于C++,而是适用于所有语言,包括Python/Ruby。它使用函数调用(“raw_input()”或“STDIN.gets”),通常会从stdin返回一个字符串,但最终却对该字符串进行了字符串替换。我猜查找getline函数的正则表达式匹配太难了,所以他们选择了在C/C++中使用gets(stdin)。 - Stobor
4
@Stobor dang,你说的对。我可以确认这种情况也发生在Java中,即使s未定义,代码System.out.print(FirstFactorial(s.nextLine()9));仍会输出89 - alter_igel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接