能否重载一个函数,可以区分定长数组和指针?

27

动机:

我几乎是出于兴趣,正在尝试编写一个函数重载,可以区分参数是固定大小的数组还是指针。

double const  d[] = {1.,2.,3.};
double a;
double const* p = &a;
f(d); // call array version
f(p); // call pointer version

我觉得这特别困难,因为一个众所周知的事实是数组很快就会衰变成指针。一个天真的方法是写下:

void f(double const* c){...}
template<size_t N> void f(double const(&a)[N]){...}

不幸的是,这并不起作用。因为在最好的情况下,编译器会将上面的数组调用f(d)确定为模糊。

部分解决方案:

我尝试了很多方法,最接近的是以下具体代码。请注意,在此示例代码中,我使用char而不是double,但最终结果非常相似。

首先,我必须使用SFINAE来禁用指针版本函数中的转换(从数组引用到指针)。其次,我必须手动重载所有可能的数组大小。

[可编译的代码]

#include<type_traits> // for enable_if (use boost enable_if in C++98)
#include<iostream>
template<class Char, typename = typename std::enable_if<std::is_same<Char, char>::value>::type>
void f(Char const* dptr){std::cout << "ptr" << std::endl;} // preferred it seems

void f(char const (&darr)[0] ){std::cout << "const arr" << std::endl;}
void f(char const (&darr)[1] ){std::cout << "const arr" << std::endl;}
void f(char const (&darr)[2] ){std::cout << "const arr" << std::endl;}
void f(char const (&darr)[3] ){std::cout << "const arr" << std::endl;}
void f(char const (&darr)[4] ){std::cout << "const arr" << std::endl;}
void f(char const (&darr)[5] ){std::cout << "const arr" << std::endl;}
void f(char const (&darr)[6] ){std::cout << "const arr" << std::endl;} // this is the one called in this particular example
// ad infinitum ...

int main(){     
    f("hello"); // print ptr, ok because this is the fixed size array
    f(std::string("hello").c_str()); // print arr, ok because `c_str()` is a pointer
}

这种方法可行,但问题在于我必须为所有可能的N值重复函数,并且使用template<size_t N>会使两个调用回到同一起点。

换句话说,template<size_t N> void f(char const(&a)[N]){std::cout << "const arr" << std::endl;}并不能帮助解决问题。

有没有办法在不返回到模棱两可的呼叫的情况下概括第二个重载?或者还有其他方法吗?

欢迎提供C ++或C ++1XYZ答案。

两个细节:1)我在上面的实验中使用了clang,2)实际的f最终将成为一个operator<<,我认为对解决方案是否有影响。


解决方案摘要(基于其他人下面的)并适应示例的具体类型char。两者都似乎依赖于使char const*指针对编译器不太明显:

  1. 一种奇怪(可移植?)的方法(来自@dyp的评论)。在指针版本中添加引用限定符:
    template<class Char, typename = typename std::enable_if<std::is_same<Char, char>::value>::type>
    void f(Char const* const& dptr){std::cout << "ptr" << std::endl;}
    
    template<size_t N>
    void f(char const (&darr)[N] ){std::cout << "const arr" << std::endl;}
  1. 一个优雅的例子(@user657267提供的特殊情况)
    template<class CharConstPtr, typename = typename std::enable_if<std::is_same<CharConstPtr, char const*>::value>::type>
    void f(CharConstPtr dptr){std::cout << "ptr" << std::endl;}
    
    template<size_t N>
    void f(char const (&darr)[N] ){std::cout << "const arr" << std::endl;}

使用std::array代替原始数组怎么样? - user657267
3
有一个奇怪的解决方法:http://coliru.stacked-crooked.com/a/37214a2859f7e78d 更明显的解决方案是使用标签派发。 - dyp
@Thomas,我在“main”代码中尝试了这个,但两者都是“const”,所以我无法利用它,我想。 - alfC
在模板参数推导期间,使用const引用获取指针可以阻止数组转换为指针的退化。 - T.C.
@T.C.,如果涉及到模板推导,那么仅仅写void f(char const* const& dptr){std::cout << "ptr" << std::endl;}并不能解决问题。至少在我的 clang++ 中是这样的。 - alfC
显示剩余6条评论
6个回答

31

这对我来说似乎有效

#include <iostream>

template<typename T>
std::enable_if_t<std::is_pointer<T>::value>
foo(T) { std::cout << "pointer\n"; }

template<typename T, std::size_t sz>
void foo(T(&)[sz]) { std::cout << "array\n"; }

int main()
{
  char const* c;
  foo(c);
  foo("hello");
}

奖励 std::experimental::type_traits:

using std::experimental::is_pointer_v;
std::enable_if_t<is_pointer_v<T>>

您的评论让我尝试了更简单的方法

template<typename T> void foo(T) { std::cout << "pointer\n"; }
template<typename T, unsigned sz> void foo(T(&)[sz]) { std::cout << "array\n"; }

当然,这里的问题在于foo现在可适用于任何类型,这取决于您希望参数检查的严格程度。
另一种方法是(滥用)右值引用。
void foo(char const*&) { std::cout << "pointer\n"; }
void foo(char const*&&) { std::cout << "array\n"; }

显然,这并非绝对可靠。

太好了,你做到了,只要从参数类型中显式地移除指针,衰减就不会发生。我在问题中发布了解决方案,以我的原始代码风格(特别是对于char类型)。 - alfC
回复您的修改:实际上那不是个问题,可以看看我上面总结的解决方案。(我提供了一个仅基于char重载的具体示例) - alfC
注意:std::experimental 中的东西不是“c++1z”或任何草案/未来标准的一部分,它们在一个单独的命名空间中有其原因。 - Jonathan Wakely
@JonathanWakely 看了一下N3932,我不确定 _v 类型是否应该放在 experimental 中,它们不是直接添加到库中的吗? - user657267
1
@user657267,这就是提案所说的,但委员会批准的并不是这样。它们没有被添加到C++工作文件中,而是进入了Library Fundamentals TS,因此在命名空间std::experimental中。 - Jonathan Wakely

9
你可以使用以下内容:
namespace detail
{
    template <typename T> struct helper;

    template <typename T> struct helper<T*> { void operator() () const {std::cout << "pointer\n";} };
    template <typename T, std::size_t N> struct helper<T[N]> { void operator() ()const {std::cout << "array\n";} };
}


template <typename T>
void f(const T& )
{
    detail::helper<T>{}();
}

Live example


谢谢,你的解决方案可能是最“正式”的。两个评论:1)成员函数可以是static(例如称为doit),2)特化可以针对具体类型(在我的情况下是char),如struct helper<char const*>......size_t N> struct helper<char const[N]>... - alfC
1
这看起来更漂亮。它更像是模式匹配,而不是像std::enable_if中的if语句。 - Siyuan Ren

7

我喜欢使用标签派发:

void foo(char const*, std::true_type /*is_pointer*/) {
  std::cout << "is pointer\n";
}
template<class T, size_t N>
void foo( T(&)[N], std::false_type /*is_pointer*/) {
  std::cout << "is array\n";
}
template<class X>
void foo( X&& x ) {
  foo( std::forward<X>(x), std::is_pointer<std::remove_reference_t<X>>{} );
}

live example


1
谢谢,我猜第二个函数中的模板 T 如果只想针对 char 使用,则不需要。template<size_t N> void foo(char const(&)[N], std::false_type){...} - alfC

6

这里有一个简单的解决方案,它利用了在C语言中,数组的值等于其地址,而对于指针来说通常并非如此。

#include <iostream>

template <typename P>
bool is_array(const P & p) {
  return &p == reinterpret_cast<const P*>(p);
}

int main() {
  int a[] = {1,2,3};
  int * p = a;

  std::cout << "a is " << (is_array(a) ? "array" : "pointer") << "\n";
  std::cout << "p is " << (is_array(p) ? "array" : "pointer") << "\n";
  std::cout << "\"hello\" is " << (is_array("hello") ? "array" : "pointer");
}

请注意,指针通常指向与其本身不同的位置,但这并不一定保证;实际上,您可以通过执行以下操作轻松欺骗代码:
//weird nasty pointer that looks like an array:
int * z = reinterpret_cast<int*>(&z); 

然而,由于你是出于兴趣编写代码,这可以是一种有趣的、基础的、初步的方法。


目标是调用一个不同的重载函数(这意味着它必须在编译时解析)...不确定您的 is_array 是否属于此类,即使您将其声明为constexpr - M.M
@MattMcNabb 我同意你的看法,我不确定目标是创建重载还是仅仅区分数组和指针... - DarioP
有趣的运行时检查和有趣的事实。实际上,对于我所考虑的完整示例,运行时检查已经足够了,因为最终大部分函数对指针和数组都是相同的。实际上,由于其中一个或另一个的元素是相同类型的,因此函数“通用性”的需求只是表面的。但是,1)如果返回类型取决于参数类型(数组或指针),则编译时版本似乎仍然适用。2)在函数is_array中无论如何都要使用模板。 - alfC
@alfC 我使用模板是因为这样可以检查任何类型的指针。如果你只有一个类型,你可以轻松地移除模板。 - DarioP

1
在我看来,这很简单:
#include<iostream>
using namespace std;

template<typename T>
void foo(T const* t)
{
  cout << "pointer" << endl;
}

template<typename T, size_t n>
void foo(T(&)[n])
{
  cout << "array" << endl;
}

int main() {
  int a[5] = {0};
  int *p = a;
  foo(a);
  foo(p);
}

我不明白为什么要使用std::enable_ifstd::true_typestd::is_pointer等魔法来复杂化编程。如果我的方法有问题,请告诉我。


1
只要参数不是 const(参见@Thomas的评论),它就有效,这是我想要覆盖的一个重要情况(因为字符串字面量是常量)。 - alfC
@alfC 我的解决方案在字符串字面量上调用时会打印出 "array"。这不是你预期的吗?我的解决方案失败的情况是什么? - marczellm
是的,它确实可以,但如果数组是“const”,那么它就不太明确了。我也想让它在这方面更加健壮。 - alfC

0

我也曾经想知道如何解决编译器抱怨重载模糊的问题,后来发现了这个方法。

这个 C++11 的解决方案与分派答案类似,但是使用了可变参数 SFINAE 技巧(编译器首先尝试数组版本)。"decltype" 部分允许不同的返回类型。如果所需的返回类型是固定的(例如 OP 中的 "void"),则不需要它。

#include <functional>
#include <iostream>
using std::cout;
using std::endl;

template <typename T> T _thing1(T* x, ...) { cout << "Pointer, x=" << x << endl; return x[0]; }
template <typename T, unsigned N> T _thing1(T (&x)[N], int) { cout << "Array, x=" << x << ", N=" << N << endl; return x[0]; }
template <typename T> auto thing1(T&& x) -> decltype(_thing1(std::forward<T>(x), 0)) { _thing1(std::forward<T>(x), 0); }

int main(int argc, char** argv)
{
    const int x0[20] = {101,2,3};
    cout << "return=" << thing1(x0) << endl;

    int x1[10] = {22};
    cout << "return=" << thing1(x1) << endl;

    float x2 = 3.141;
    cout << "return=" << thing1(&x2) << endl;

    const float x3 = 55.1;
    cout << "return=" << thing1(&x3) << endl;

}

示例输出为:

$ g++ -std=c++11 array_vs_ptr.cpp -o array_vs_ptr && ./array_vs_ptr
Array, x=0x22ca90, N=20
return=101
Array, x=0x22ca60, N=10
return=22
Pointer, x=0x22caec
return=3.141
Pointer, x=0x22cae8
return=55.1

有趣的解决方案。但是,由于可以通过在第一个函数中添加 const& 来使示例无需使用省略号即可正常工作,因此这是一种过度设计。此时将减少到其他解决方案:template <typename T> T _thing1(T* const& x){ cout << “Pointer,x =”<< x << endl; return x [0]; } template <typename T,unsigned N> T _thing1(T(&x)[N]){ cout << “Array,x =”<< x << “,N =”<< N << endl; return x [0]; } template <typename T> auto thing1(T && x)-> decltype(_thing1(std :: forward <T>(x))){ return _thing1(std :: forward <T>(x)); } - alfC

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