文章目录
问题描述
最近敲代码的时候,遇到一个有趣的问题,特此记录下。
问题:C++中的头文件没有在文件最开头引入,而是在自定义的命名空间中引入,这会导致哪些问题。
比如下面的问题示例:
#include <iostream>
#define NUMERIC
namespace math {
#ifdef NUMERIC
# include <numeric>
#endif
}
int main(int argc, char* argv[])
{
std::cout<<"hello world"<<std::endl;
}
编译这个代码,会出现如下报错:
➜ g++ -o ns ns.cpp
In file included from /usr/include/c++/9/numeric:62,
from ns.cpp:8:
/usr/include/c++/9/bits/stl_numeric.h: In function ‘_OutputIterator math::std::partial_sum(_InputIterator, _InputIterator, _OutputIterator)’:
/usr/include/c++/9/bits/stl_numeric.h:253:24: error: expected nested-name-specifier before ‘iterator_traits’
253 | typedef typename iterator_traits<_InputIterator>::value_type _ValueType;
| ^~~~~~~~~~~~~~~
......
......
/usr/include/c++/9/bits/stl_numeric.h:349:14: error: ‘move’ is not a member of ‘math::std’; did you mean ‘std::move’?
349 | __value = _GLIBCXX_MOVE(__tmp);
| ^~~~~~~~~~~~~
......
......
PS:我没能完全解决这个问题,问题有些难。
问题分析
报错一:expected nested-name-specifier before
当我们使用搜素引擎,搜素expected nested-name-specifier before
这个问题的时候,我目前只找见nested-name-specifier - stackoverflow,里面的提示是删除typename
。这不是我想要的内容,因为不应该修改标准库的内容。
所以,我们只好自己分析这个问题。下面为报错处的上下文代码。
template<typename _InputIterator, typename _OutputIterator,
typename _BinaryOperation>
_OutputIterator
partial_sum(_InputIterator __first, _InputIterator __last,
_OutputIterator __result, _BinaryOperation __binary_op)
{
typedef typename iterator_traits<_InputIterator>::value_type _ValueType;
// concept requirements
__glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
__glibcxx_function_requires(_OutputIteratorConcept<_OutputIterator,
_ValueType>)
__glibcxx_requires_valid_range(__first, __last);
if (__first == __last)
return __result;
_ValueType __value = *__first;
*__result = __value;
while (++__first != __last)
{
__value = __binary_op(_GLIBCXX_MOVE_IF_20(__value), *__first);
*++__result = __value;
}
return ++__result;
}
std::partial_sum函数作用有点类似于求前缀和,整个函数使用模板方式实现。
typedef typename iterator_traits<_InputIterator>::value_type _ValueType;
引起的报错。其中的typename,在模板的定义中, can be used to declare that a dependent qualified name is a type. (即,iterator_traits<_InputIterator>::value_type
的类型是不确定的,依赖_InputIterator
。)
然后,我就不知道了。万一哪天需要,我翻看完《C++ Templates中文版》,我再回来填坑。
填坑中…
-
假定T是一个模板类型参数,当编译器遇到类似T::mem这样的代码时,它不会知道mem是一个类型成员还是一个static数据成员,直至实例化时才会知道。但是,为了处理模板,编译器必须知道名字是否表示一个类型。
因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字typename来实现这一点。
报错二:‘move’ is not a member of ‘math::std’; did you mean ‘std::move’
这个问题,大体是域名空间的嵌套与调用。
__value = _GLIBCXX_MOVE(__tmp);
中_GLIBCXX_MOVE
宏定义如下所示:
#define _GLIBCXX_MOVE(__val) std::move(__val)
由于外层嵌套的域名,原本需要调用的是std::move
,变成了math::std::move
。
这个问题该怎么解释呢?先挖个坑,回头来填。
回来填坑中…
详细参见《C++ Primer》18.2 命名空间。
-
在通常情况下,我们不把#include放在命名空间内部。如果我们这么做了,隐含的意思是把头文件中所有的名字定义成该命名空间的成员。(上面那么做,意味着我们试图将numeric头文件中std函数嵌套在命名空间math中。)
-
嵌套的命名空间同时是一个嵌套的作用域,它嵌套在外层命名空间的作用域中。嵌套的命名空间中的名字遵循的规则与往常类似:内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码要想访问它必须在名字前添加限定符。
此时,我们会奇怪。上面代码,由于头文件包含在命名空间见中,应该会包含math::std::move函数的。内部调用的是std::move函数,应该会查找到math::std::move,而不应该报错的。
我们使用
g++ -E
进行头文件展开,可以明显看到有math::std::move函数。
命名空间中的内容,调用该命名空间的嵌套命名空间中的函数,不需要在函数前面加上完整的作用域。为此,我做了下验证。
此时,发生上面报错的原因,则可能是由于move函数的特殊性。
下面两点是相关性的两点,并不能直接说明问题。详细问题的解释,我不清楚。 -
当我们给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间。这一例外对于传递类的引用或指针的调用同样有效。查找规则的这个例外允许概念上作为类接口一部分的非成员函数无须单独的using声明就能被程序使用。
使用g++ -E
我们会发现:#inlcude<iostream>
会引入std::move
;在math命名空间中包含# include <numeric>
会引入math::std::mov
。
解决方法
按照这条准则写代码:头文件不要包含在命名空间中,在开头调用。
#include <iostream>
#define NUMERIC
#ifdef NUMERIC
# include <numeric>
#endif
namespace math {
//
}
int main(int argc, char* argv[])
{
std::cout<<"hello world"<<std::endl;
}