【问题标题】:Deducing the template parameter of a templated class argument: const issue推导模板化类参数的模板参数:const 问题
【发布时间】:2025-12-20 00:40:11
【问题描述】:

我认为这个问题相当普遍,所以应该有一个已知的解决方案。我想出了一个,但我不是很满意,所以我在这里问,希望有人能帮忙。

假设我有一个函数,其签名是

template<typename T>
void foo(const MyArray<const T>& x);

模板参数中的 const 是为了防止我更改数组内容,因为(出于此问题之外的原因),MyArray&lt;T&gt; 的访问器([]())始终标记为 const,并返回对T 的引用(因此,const 确保安全,因为MyArray&lt;T&gt;::operator[] 返回T&amp;,而MyArray&lt;const T&gt;::operator[] 返回const T&amp;)。

太好了。但是,具有不同模板参数的模板是不相关的,所以我不能将 MyClass&lt;T&gt; 的引用绑定到 MyClass&lt;const T&gt; 的引用,这意味着我不能这样做

MyArray<double> ar(/*blah*/);
foo(ar);

请注意,如果没有引用,上面的代码将可以工作如果有一个复制构造函数可以让我从MyArray&lt;T&gt; 创建MyArray&lt;const T&gt;。但是,我不想删除引用,因为数组构造会发生很多次,而且尽管相对便宜,但它的成本会增加。

所以问题是:我如何用MyArray&lt;T&gt; 调用foo

到目前为止,我唯一的解决方案如下:

MyArray<T> ar(/*blah*/);
foo(reinterpret_cast<MyArray<const T>>(ar));

(实际上在我的代码中,我将重新解释转换隐藏在具有更详细名称的内联函数中,但最终游戏是相同的)。 MyArray 类没有对 const T 的专门化,这使得它无法重新解释,因此演员表应该是“安全的”。但这并不是一个很好的阅读解决方案。另一种方法是复制foo 的签名,以获得采用MyArray&lt;T&gt; 的版本,该实现执行转换并调用const 版本。这个问题是代码重复(我有很多函数foo需要重复)。

也许foo 的签名上有一些额外的模板魔法?目标是同时传递 MyArray&lt;T&gt;MyArray&lt;const T&gt;,同时仍保持 const 正确性(即,如果我不小心更改了函数体中的输入,则使编译器崩溃)。

编辑 1MyArray 类(其实现不受我控制)具有 const 访问器,因为它存储指针。所以调用v[3] 将修改数组中的值,但不会修改存储在类中的成员(即指针和一些类似智能指针的元数据)。换句话说,对象实际上没有被访问器修改,尽管数组是。这是语义上的区别。不知道他们为什么要朝这个方向发展(我有一个想法,但解释的时间太长了)。

编辑 2:我接受了两个答案之一(尽管它们有些相似)。我不确定(出于长时间解释的原因)包装类在我的情况下是否可行(也许,我必须考虑一下)。我也很困惑,虽然

template<typename T>
void foo(const MyArray<const T>& x);
MyArray<int> a;
foo(a);

不编译,下面可以

void foo(const MyArray<const int>& x);
MyArray<int> a;
foo(a);

注意:MyArray 确实提供了带有签名的模板化“复制构造函数”

template<typename S>
MyArray(const MyArray<S>&);

所以它可以从MyArray&lt;T&gt; 创建MyArray&lt;const T&gt;。我很困惑为什么当T 是显式时它会起作用,而如果T 是模板参数则它不起作用。

【问题讨论】:

  • 如果访问者是const,他们不应该返回一个常量引用吗?那么你就不需要const T 模板参数了。
  • 好问题。查看我的编辑。
  • 为什么不能简单地使用template&lt;class T&gt; void foo(const MyArray&lt;T&gt;&amp; x);
  • @Evg,如果我这样做,那么编译器无法帮助我检测数组的意外更改(例如,由于拼写错误)。在执行MyArray&lt;int&gt; a; foo(a); 时,编译器将允许在foo 中更改a。我想通过对输入强制执行 const 来防止这种情况发生。
  • 对于edit2,你应该正常问另一个问题,但是对于模板函数,参数应该“匹配”,而对于非模板函数,允许一个用户转换(通过参数)。

标签: c++ templates constants template-argument-deduction


【解决方案1】:

我会留下来

template<typename T>
void foo(const MyArray<T>&);

并确保使用 const T 实例化它(例如在 unitTest 中)。

否则,您可以将视图创建为std::span

类似的东西(取决于MyArray提供的其他方法,您可能可以做一个更好的const视图。我目前只使用operator[]):

template <typename T>
struct MyArrayConstView
{
    MyArrayConstView(MyArray<T>& array) : mArray(std::ref(array)) {}
    MyArrayConstView(MyArray<const T>& array) : mArray(std::ref(array)) {}

    const T& operator[](std::size_t i) {
        return std::visit([i](const auto& a) -> const T& { return a[i]; }), mArray);
    }

private:
    std::variant<std::reference_wrapper<MyArray<T>>,
                 std::reference_wrapper<MyArray<const T>>> mArray;
};

然后

template <typename T>
void foo(const MyArrayConstView<T>&);

但你需要明确地调用它,(因为MyArray&lt;T&gt; 不是MyArrayConstView 不会发生推断)

MyArray<double> ar(/*blah*/);
foo(MyArrayConstView{ar});
foo<double>(ar);

【讨论】:

  • MyArray 非常接近std::span(由于可移植性和编译器限制,我不允许使用它)。
【解决方案2】:

由于不允许更改 MyArray,因此一种选择是使用适配器类。

template <typename T>
class ConstMyArrayView {
   public:
    // Not an explicit constructor!
    ConstMyArrayView(const MyArray<T>& a) : a_(a) {}
    const T& operator[](size_t i) const { return a_[i]; }
   private:
     const MyArray<T>& a_;
};

template<typename T>
void foo(const ConstMyArrayView<T>& x);

MyArray<T> x;
foo(x);

但最后,如果您可以更改 MyArray 以匹配您想要的 const 正确性,或者切换到一个类,那将是更好的选择。

【讨论】:

  • 不。 const MyArray& 将在没有任何干预的情况下生成一个 ConstMyArrayView
  • @IdeaHat: 因为foo 是模板,确实不需要额外的构造函数,但是MyArray&lt;T&gt; x; foo(x); 不会编译,因为它不匹配。 foo&lt;T&gt;(x);foo(ConstMyArrayView {x}) 将是必需的。
  • 但如果foo 不是模板,则可能会调用重载;)但void foo(const ConstMyArrayView&lt;int&gt;&amp; x); 不会接受MyArray&lt;const int&gt;
【解决方案3】:

这是一种丑陋但有效的方法,让函数使用一种类型,但也让编译器检查如果它使用不同的类型,相同的代码是否会编译:

template <typename From, typename To>
struct xfer_refs_cv
{
    using type = To;
};
template <typename From, typename To>
struct xfer_refs_cv<const From, To>
{
    using type = const typename xfer_refs_cv<From, To>::type;
};
template <typename From, typename To>
struct xfer_refs_cv<volatile From, To>
{
    using type = volatile typename xfer_refs_cv<From, To>::type;
};
template <typename From, typename To>
struct xfer_refs_cv<From&, To>
{
    using type = typename xfer_refs_cv<From, To>::type&;
};
template <typename From, typename To>
struct xfer_refs_cv<From&&, To>
{
    using type = typename xfer_refs_cv<From, To>::type&&;
};

template <typename CheckType, typename Func, typename CallType>
constexpr decltype(auto) check_and_call(Func&& f, CallType&& call_arg)
    noexcept(noexcept(std::forward<Func>(f)(std::forward<CallType>(call_arg))))
{
    (void) decltype(std::declval<Func&&>()
      (std::declval<typename xfer_refs_cv<CallType&&, CheckType>::type>()), 0){};
    return std::forward<Func>(f)(std::forward<CallType>(call_arg));
}

template<typename T>
void foo(const MyArray<T>& x)
{
    check_and_call<MyArray<const T>>(
        [](auto&& x) {
        // Function implementation here.
    }, x);
}

【讨论】: