我遇到以下问题,由于 lambda 的自动参数而选择了错误的方法。

请参阅以下示例:

template <class T, class FunctionT>
concept ValidNonConstSignature = requires(FunctionT exec_fn, T& value) { exec_fn(value); };

template <class T, class FunctionT>
concept ValidConstSignature = requires(FunctionT exec_fn, const T& value) { exec_fn(value); };

class Foo {
public:
  template <class FunctionT> requires ValidNonConstSignature<int, FunctionT>
  auto
  getBar(FunctionT callback) {
    return callback(m_abc);
  }

  template <class FunctionT> requires ValidConstSignature<int, FunctionT>
  auto
  getBar(FunctionT callback) const {
    return callback(m_abc);
  }

private:
  int m_abc {};
};

int main()
{
    Foo foo;
    
    foo.getBar([](int& abc){ return abc++; });
    foo.getBar([](const int& abc){ return abc++; });   // correct compiler error: increment of read-only reference ‘abc’
    
    foo.getBar([](auto& abc){ return abc++; });        // incorrect compiler error: increment of read-only reference ‘abc’
    foo.getBar([](const auto& abc){ return abc++; });  // correct compiler error: increment of read-only reference ‘abc’

    return 0;
}

由于 的存在getBar(FunctionT callback) constfoo.getBar([](auto& abc){ return abc++; });导致 编译失败。

如果getBar(FunctionT callback) const删除该方法,foo.getBar([](auto& abc){ return abc++; });编译就成功了。

有没有办法以某种方式增强概念以正确选择non-const方法?我最初的想法是通过传递的回调中的另一个模板获取参数类型,但是当传递带有 lamda 时,编译器会抱怨类型不完整auto

10

  • 1
    当您使用带引用的 auto 时,可以将其推断为 const 引用(与 auto 没有引用时不同)。因此,问题不在您的概念中,而auto&可以推断为const int&int&


    – 

  • 4
    [](auto& abc){ return abc++; }不是 SFINAE 友好的,你可能需要!std::is_const_v<std::remove_reference_t<decltype(abc)>>


    – 


  • ValidNonConstSignature按照ValidConstSignature:来定义concept ValidNonConstSignature = ValidConstSignature<T, F> or requires。这比ValidNonConstSignature更具体ValidConstSignature。更好的选择是新的显式语法decltype(auto) Foo::getbar(this auto& self, auto&& f) requires std::invocable<decltype(f), decltype(self.m_abc)> { return f(self.m_abc)};


    – 

  • @Red.Wave:问题出现在:评估ValidConstSignature<int, decltype([](auto& abc) { return abc++; })>触发硬错误,而不是替换失败。


    – 

  • @ Red.WaveA or B不如A


    – 


最佳答案
3

如果您无法使用 C++23,您可以像这样模拟推导

#include <type_traits>

template <class T, class FunctionT>
concept ValidSignature =
    requires(FunctionT exec_fn, T& value) { exec_fn(value); };

class Foo
{
  public:
    template <class This, class FunctionT>
    requires std::is_same_v<std::decay_t<This>, Foo>
             && ValidSignature<std::conditional_t<std::is_const_v<This>, 
                                                  const int, int>, FunctionT>
    friend auto getBar(This& foo, FunctionT callback) {
        return callback(foo.m_abc);
    }

  private:
    int m_abc{};
};

int main() {
    Foo foo;
    const Foo foo_const;

    getBar(foo, [](int& abc) { return abc++; });
    // getBar(foo, [](const int& abc) { return abc++; }); // does not compile

    getBar(foo, [](auto& abc) { return abc++; });
    // getBar(foo, [](const auto& abc) { return abc++; }); // does not compile

    // getBar(foo_const, [](int& abc) { return abc++; }); // does not compile
    getBar(foo_const, [](const int& abc) { return abc; });

    // getBar(foo_const, [](auto& abc) { return abc++; }); // does not compile
    getBar(foo_const, [](const auto& abc) { return abc; });
}

请注意,您并不严格需要std::conditional_t<std::is_const_v<This>, const int, int>,但硬错误将发生在不同的地方。

在Red.Wave建议明确的 this之后,我或多或少地决定采用这种可以重用实现的方法(类似于patrick的示例,但更简洁一些):

C++20

#include <concepts>
#include <type_traits>
#include <utility>

template <class T, class FunctionT>
concept ValidSignature =
    requires(FunctionT exec_fn, T& value) { exec_fn(value); };

class Foo {
   public:
    template <class FunctionT>
    auto getBar(FunctionT&& callback) {
        return getBarImpl(*this, std::forward<FunctionT>(callback));
    }

    template <class FunctionT>
    auto getBar(FunctionT&& callback) const {
        return getBarImpl(*this, std::forward<FunctionT>(callback));
    }

   private:
    static auto getBarImpl(auto& self, auto&& callback)
        requires ValidSignature<decltype(self.m_abc), decltype(callback)>
    {
        constexpr bool IsConst = std::is_const_v<std::remove_reference_t<decltype(self)>>;
        if constexpr (IsConst) {
            // called from const
        }
        else {
            // called from non-const
        }

        return std::forward<decltype(callback)>(callback)(self.m_abc);
    }

    int m_abc{};
};

int main() {
    Foo foo;
    const Foo foo_const;

    foo.getBar([](int& abc) { return abc++; });
    foo.getBar([](const int& abc) { return abc; });

    foo.getBar([](auto& abc) { return abc++; });
    foo.getBar([](const auto& abc) { return abc; });

    // foo_const.getBar([](int& abc) { return abc++; });
    foo_const.getBar([](const int& abc) { return abc; });

    // foo_const.getBar([](auto& abc) { return abc++; });
    foo_const.getBar([](const auto& abc) { return abc; });
}

对于未来的旅行者,你可以使用C++23做到这一点

#include <concepts>
#include <type_traits>
#include <utility>

template <class T, class FunctionT>
concept ValidSignature =
    requires(FunctionT exec_fn, T& value) { exec_fn(value); };

class Foo {
   public:
    auto getBar(this auto& self, auto&& callback)
        requires ValidSignature<decltype(self.m_abc), decltype(callback)>
    {
        constexpr bool IsConst = std::is_const_v<std::remove_reference_t<decltype(self)>>;
        if constexpr (IsConst) {
            // called from const
        }
        else {
            // called from non-const
        }

        return std::forward<decltype(callback)>(callback)(self.m_abc);
    }

   private:
    int m_abc{};
};

int main() {
    Foo foo;
    const Foo foo_const;

    foo.getBar([](int& abc) { return abc++; });
    foo.getBar([](const int& abc) { return abc; });

    foo.getBar([](auto& abc) { return abc++; });
    foo.getBar([](const auto& abc) { return abc; });

    // foo_const.getBar([](int& abc) { return abc++; });
    foo_const.getBar([](const int& abc) { return abc; });

    // foo_const.getBar([](auto& abc) { return abc++; });
    foo_const.getBar([](const auto& abc) { return abc; });
}

3

  • decltype(self.m_abc)不会给你const int但是int


    – 

  • @patrick 这为什么是个问题?self.m_abc从 const 方法调用时仍然是 const,如果您的 lambda 尝试将其修改为引用,则不会编译。


    – 

  • 1
    我想说的是,你最好写,ValidSignature<int, decltype(callback)>而不是ValidSignature<decltype(self.m_abc), decltype(callback)>因为decltype(self.m_abc)永远不会给你const int


    – 


关键问题是您的 lambda 不支持 SFINAE。这个想法是,带有auto&参数的 lambda 可能无法在概念、重载或模板约束下以可预测的方式运行,因为 lambda 不会自然地拒绝某些类型,例如const&通过替换失败。

类型不匹配问题通常很容易解释,但这个问题却不是,我会尽力解释清楚。

这是你的ValidNonConstSignature概念

template <class T, class FunctionT>
concept ValidNonConstSignature = requires(FunctionT exec_fn, T& value) { exec_fn(value); };

在requires子句中,(FunctionT exec_fn, T& value) { exec_fn(value)即lambda需要接受一个int&,这完全没问题,直到你用实例化概念T = int。概念用来验证lambda的类型变得令人困惑,因为T = int但是requires子句说exec_fn(int& value),这是问题的根源。

这个概念被实例化,T = int但是当它检查 lambda 签名时,它期望 lambda 能够一起工作int&,这就是为什么我在回答时提到你的 lambda 不是 SFINAE 友好的。

我们不能说遵循相同的模式总会导致类型不匹配。如果是这种情况,我们要么修复语言,要么避免使用它,因为我们知道它不会 100% 有效。但实际上我可以说,可能会导致类型不匹配,具体取决于 lambda 的定义方式,特别是如果您在 lambda 中使用auto&,由于类型推导规则,其行为会略有不同。

总而言之,让我们看一下你的 lambda

foo.getBar([](auto& abc) { return abc++; });
  1. lambda 参数被推导为,int&但您的概念受到 的限制ValidNonConstSignature<int, FunctionT>。这种不匹配使编译器感到困惑,因为约束需要不同的实例化(intvs int&)。

  2. 这种auto&推论导致编译器报告 lambda 签名与所需的概念不匹配,尽管从auto&
    逻辑上来说应该可以工作。

int&一个简单的解决方法就是用而不是 来实例化概念int

这确保引用语义得到正确保存,并且当传递以下 lambda 时:

foo.getBar([](auto& abc) { return abc++; });

编译器知道auto推导为int&并且这与约束int&中的相匹配ValidNonConstSignature,因此概念检查成功。

这里是概念实例化的变化。

class Foo {
public:
  template <class FunctionT> requires ValidNonConstSignature<int&, FunctionT>
  auto
  getBar(FunctionT callback) {
    return callback(m_abc);
  }

  template <class FunctionT> requires ValidConstSignature<int&, FunctionT>
  auto
  getBar(FunctionT callback) const {
    return callback(m_abc);
  }

private:
  int m_abc {};
};

6

  • 它也只适用于 1 个概念,没有int&。但是,现在我的下一步是尝试重用非常量方法,这本身就是另一个问题:


    – 

  • @FrogTheFrog 我的错,我在发布答案后才注意到这一点。我正在研究这个问题,一旦找到解决方案,我就会更新它


    – 

  • 没问题,谢谢你的帮助!我确实有一个“解决方案”,但我不知道我对此有何感受……


    – 

  • 1
    @FrogTheFrog 通常,非 const 版本是根据 const 版本实现的,而不是相反。将非 const 转换为 const 比将非 const 转换为 const 要安全得多。缩小函数范围而不是扩大函数范围也更为明智。


    – 

  • @FrogTheFrog 我不知道这是否有用,但这是我的看法。当 lambda sig 包含 const 时,我能够将实体 static_cast 为 const 来调用 const 实例。但问题auto&仍然存在。也许将我的试验与您的试验结合起来可以解决问题


    –