其实我感觉go除了不能用const 限制函数参数不可变和编译器宏以外,其实真算不上简陋,相反我感觉他的并发模型非常的强大。
go除了iota以外给我的感觉是一切都是特别清晰的,不会像c++那样有一些犄角旮旯的东西。c++甚至有些东西的决定权是交给编译器实现的,这点增加了不确定性和理解成本。
在go里面几乎所有的理论是从头到尾去贯彻的,我的意思是你很难见到一些special case。这点在代码的可维护性和可读性上非常重要。
go的一些特性缺失在我看来其实是暗合“设计原则”的要求的,这一点你多用,多去抽象代码应该就会有一些体会的。很多特性的缺失其实稍稍动动脑筋就可以想出既容易理解,也容易实现的方法。
go给我有点less is more的感觉。go的代码是你稍微学学,你就能看懂了。c++的代码是,你就算读完《c++ primer》 你拿到商业代码别人把设计思路给你讲一遍你也不一定能看得懂(一堆的条件编译,各种类型搞得人很头疼ts也是很多库函数的签名看的都费劲)。去看看有些STL的源码,函数重载的情况下到底走的哪个构造函数有时候都看半天都一脸蒙。更别说里面还有各种跟c++标准相关的条件编译在里面。这是unreal engine 里面关于vector的构造函数,你看了应该就大概明白了(你可以看看go的标准库的代码,结合着看看,只需一眼)。
_LIBCPP_CONSTEXPR_AFTER_CXX17 _LIBCPP_INLINE_VISIBILITY
vector() _NOEXCEPT_(is_nothrow_default_constructible<allocator_type>::value)
{
_VSTD::__debug_db_insert_c(this);
}
_LIBCPP_CONSTEXPR_AFTER_CXX17 _LIBCPP_INLINE_VISIBILITY explicit vector(const allocator_type& __a)
#if _LIBCPP_STD_VER <= 14
_NOEXCEPT_(is_nothrow_copy_constructible<allocator_type>::value)
#else
_NOEXCEPT
#endif
: __end_cap_(nullptr, __a)
{
_VSTD::__debug_db_insert_c(this);
}
_LIBCPP_CONSTEXPR_AFTER_CXX17 explicit vector(size_type __n);
#if _LIBCPP_STD_VER > 11
_LIBCPP_CONSTEXPR_AFTER_CXX17 explicit vector(size_type __n, const allocator_type& __a);
#endif
_LIBCPP_CONSTEXPR_AFTER_CXX17 vector(size_type __n, const value_type& __x);
template <class = __enable_if_t<__is_allocator<_Allocator>::value> >
_LIBCPP_CONSTEXPR_AFTER_CXX17
vector(size_type __n, const value_type& __x, const allocator_type& __a)
: __end_cap_(nullptr, __a)
{
_VSTD::__debug_db_insert_c(this);
if (__n > 0)
{
__vallocate(__n);
__construct_at_end(__n, __x);
}
}
template <class _InputIterator>
_LIBCPP_CONSTEXPR_AFTER_CXX17
vector(_InputIterator __first,
typename enable_if<__is_exactly_cpp17_input_iterator<_InputIterator>::value &&
is_constructible<
value_type,
typename iterator_traits<_InputIterator>::reference>::value,
_InputIterator>::type __last);
template <class _InputIterator>
_LIBCPP_CONSTEXPR_AFTER_CXX17
vector(_InputIterator __first, _InputIterator __last, const allocator_type& __a,
typename enable_if<__is_exactly_cpp17_input_iterator<_InputIterator>::value &&
is_constructible<
value_type,
typename iterator_traits<_InputIterator>::reference>::value>::type* = 0);
go在一开始就进行了非常优秀的设计,所以你会发现这么多年过去了没有什么沉重的历史包袱需要像c++那样一个版本一个版本的不断打补丁最后还不一定达得到效果。for中对零时变量的重用可能算一个吧,但是新版本已经改了。
go属于短小但是精湛,cpp/ts这种给我感觉是大而全,但是有可能做的越多漏洞越多,差不多是这么个情况吧。
btw写多了go再去写cpp这种真的有种被惯坏的感觉,但是我下家入职的公司是要写cpp的,头疼
只要你遵循标准, 就不会有不确定性 (除非编译器有 bug, 虽然这很常见) 和理解成本. 不过标准本身就需要花很多时间去理解就是了…
你不可以忽略它为这个优点而牺牲了很多语言特性. 无论什么语言, 特性的多少和入门门槛是相斥的.
STL 的设计目的就不是给用户照着源码学习用的. 想查用法应该看文档. 而且标准本身没规定所有的实现细节.
Go 的标准库是一家独大, 并且有给用户参考的目的.
你知道我要说什么.
这个已经很普及了.
但确实写起来爽得不行, go 一下就好.
我受不了 Go 的 breaking change. 它口口声声说自己有兼容性承诺, 结果经常有语义发生变化的情况.
通常在 C++ 中会直接不通过编译, 或者以 deprecated → removed 的方式. 而 Go 里面你不去看 NEWS 根本不知道有变化, 比如下面这段 (太久不写 Go 了, 我混合 Python 的语法, 你凑合着看):
f: func() int
for i := range(10) {
if i == 0:
f = func() { return i }
}
print(f())
这段代码在不同版本的 Go 之间输出是不一样的.
类似的重大 breaking change 还有 Goroutine 的调度方式, 太多了.
当然, 我喜欢 Go 胜过那个 GitHub 链接里提到的 Rust 和 C#. Go 随便写点东西挺方便的.
这个问题还真不是这样的,我最近看的cpp书籍里面就不止提到过一次"一些细节是编译器决定的"
这一点我同意,但是我想说的是go的设计哲学并不是走的大而全的路线。go的策略更加偏向简洁,而简洁往往意味着更好的理解,更加笃定,和更少的沟通损耗。也就是说,我们可以更自信的编程,更自信的沟通,更自信的去理解别人的代码。
并不是出于学习目的是不错,但是好的代码不正应该能够去自描述吗。即使不是STL的源码,我也希望项目组中写的函数签名能够一眼被识别。不是所有项目组在工程迭代中都有足够时间和精力去维护一份文档的
go在新版本中,更改的原因是:跨越了很多年整个社区都知道这是一种应该注意的问题,或者说社区已经形成共识了,大家都在极力避免这种写法。既然大家都极力避免了,go也就没必要再去支持这种写法了。而且我听我的一个朋友说,每个go的版本迭代,google都要在自己的所有go代码仓库中做一次单元测试确保不会有问题才会做这种变更。go加特性都非常克制,这种变动怎么会不小心翼翼?
啥细节?
老生长谈:
total = ++count * 3 + count++ * 5;
char的符号性
int到底多大
btw这些我有点耍无赖的嫌疑了 。
但是写cpp头会秃不是众所周知的么 ,即使确定可能也会被一些规则搞得心烦。
go 的表达能力差,因此要写更多的重复工作,但是优点就是阅读代码的时候心智负担小。
感觉有没有可能 typescript-go 大量的代码是 AI 写的,因此这种重复劳动的工作其实是 AI 在做,而工程师其实只是在负责 review AI 生成的代码?
当然了,也不排除 MS 直接往 TS team 堆人力硬做这种重复劳动的活。
其实我是这么理解的,cpp很多特性是可用可不用的。但是因为复杂,编译速度就会慢,像ue编译一次我电脑得20分钟。但是go就只实现必要特性,因为很多特性是可选的,但是编译速度慢可是每次编译都会遇到的。go在一些特性的实现上还是想的很明白的。
比如函数多态,可能不走调试器我都不知道他到底走的哪个构造。但是go就不实现函数多态了,因为看代码是天天看,函数多态可不是天天用。好钢用在刀刃上。
最近在整给计算器写的 ROM,遇到过这么一个问题,在 Arm Embedded Toolchain GCC 9.3 上链接报错 undefined reference to le<long>::byte_order
,GCC 10.3 以后就没问题。
后来发现这是一个微妙的 proposal 导致的不同。因为我平时不用 C++,所以花了点功夫才找到 fix。 无奖竞猜,这个 fix 要怎么写?提示,只用改一行。
template <typename Int>
struct le
// ----------------------------------------------------------------------------
// Little endian representation for a type
// ----------------------------------------------------------------------------
{
static constexpr union bytes
{
byte bytes[sizeof(Int)];
Int value;
} byte_order = { .value = Int(0x0706050403020100ULL) };
le() : value(0)
{
}
le(Int v) : value(swap(v))
{
}
operator Int()
{
return swap(value);
}
le &operator=(Int x)
{
value = swap(x);
return *this;
}
Int swap(Int x)
{
bytes b = { .value = x };
for (uint i = 0; i < sizeof(Int); i++)
std::swap(b.bytes[i], b.bytes[byte_order.bytes[i]]);
return b.value;
}
Int value;
} PACKED;
答案见 P0386R2
但是,我也搞不清楚这是编译器 bug,还是 GCC 默认采用的标准改了。 C++ Standards Support in GCC - GNU Project 说相关特性 GCC 7 就有了。而且这个问题源头上还是 C++11 的设计失误。
这个问题在于,C++11, C++17, C++21,语法变动太多了,想要一份代码兼容,这就成了不得不付出的成本。
而“只用一个版本,那就是最新版”,对 Go 来说可能,对 C++ 來说完全不现实。
所以这就是矛盾的地方,一方面有人嫌弃 C++ 不思进取,另一方面又有人嫌弃 C++ 改动太大,几乎没有其他任何语言能同时因为不思进取和改动太大两个完全相反的原因而被嫌弃。
原本不能通过编译的代码,现在可以通过编译 — 这是宽容度变高了。
原本能通过编译的代码,现在不能通过编译(在C++中这通常是过渡完成的,比如 C++17 报 warning,C++20再报错) — 错误并没有带到运行时。
最难以接受的是刚刚提到的 Go 的那个例子,前后都能通过编译,然而运行时的语义发生了变化。注意:Google 本身是对 Go 作出了兼容性承诺的!JavaScript 解决那个问题的方法是引入了 let,真的很感人,它没有丢下老代码不管
你好,编译都是通过的,问题是出在后面的 link time error,而且报的错和语言版本特性无关,就是缺少定义时会报的 undefined symbol error。
也就是说可以理解为 C++/GCC 在这里也改变了语义,而且是一个很不明显的方式。改变发生的时间 C++ 标准也无大动作。再进一步说,改动前后 GCC 也没引入对应的 Warning,说明了什么?
所以可以认为,是因为的确影响到自已才导致你觉的难受。这当然是可以理觛的,不过,构不成说 go 这样做有什么大不妥的理由。从源头来看,C++ 和 Go 这里的改动都是为了修正过去的设计问题,我不认为有哪一方比哪一方做得更好的道理。
这个就幽默了。一来不同编译器实现不同,二来标准里的一些内容,如STL里的一些并发函数压根就还没实现。
非要绕过标准去写编译器实现相关的代码是你个人喜好问题吧 ?
不是,实践上来说 C++ 不用编译器定义行为很难写任何有实用意义的代码。 单是 MSVC 神奇的浮点数异常处理就够难受的了。写 Embbeded,标准更是没有对 memory layout 怎么 pack 有指导意义,用了 union 类型就是在用编译器实现相关特性了。
国外用.net的还是很多,尤其是做工业应用的,游戏的
github 知名的C++库,几乎都对不同平台使用不同的宏定义,例如 if defined __APPLE__
、ifdef _WIN32
等,这是现实不是个人喜好。