C++ вызывает функцию с неизвестным типом аргументов?

Я пытаюсь добиться того же, что и RapidCheck: вызывать любой Callable независимо от его аргументов.

Вот пример из RapidCheck:

#include <rapidcheck.h>

int main() {
  rc::check("Addition is commutative.",
            [](int a, int b) {
              RC_ASSERT(a + b == b + a);
            });

  return 0;
}
  • В отличие от RapidCheck, который использует случайные данные для вызова Callable, я хотел бы использовать источник данных. В частности, я снимаю uint8_t массив для любого типа мне нужно. (См. Пример ниже.)
  • Я в порядке с использованием C++17, но предпочел бы C++11. В настоящее время у меня есть только пример C++17.
  • Мой пример ниже работает для произвольного числа аргументов в Callable, но не для произвольных типов аргументов. И, конечно, не для смешанных типов аргументов.
  • Я делаю это, чтобы я мог использовать потрясающий API RapidCheck с libFuzzer из LLVM. Аналогично моему предыдущему подходу здесь ( Пример).

Что у меня пока с некоторыми комментариями ( онлайн):

// Compiles with Clang(trunk)/GCC(7.2) using -std=c++17!

#include <cassert>
#include <functional>
#include <iostream>
#include <mutex>
#include <string>
#include <type_traits>

/** Provides access to a uint8_t array as specific types.
*
* Fulfills thus the LLVMFuzzerTestOneInput-interface, which uses
* (uint8_t *Data, size_t Size) as input.
*/
class RawQueue {
public:
  RawQueue(uint8_t *Data, size_t Size)
      : data_(Data), size_(Size / sizeof(uint8_t)), index_(0){};

  /** Takes one element of type T from queue.
  *
  * Throws if empty.
  *
  * NOTE: Little endianess means that uint8_t {1, 0, 0, 0} == int {1}.
  */
  template <typename T> T pop() {
    assert(data_);
    std::scoped_lock<std::mutex> lock(data_mutex_);

    const size_t new_index = index_ + sizeof(T) / sizeof(uint8_t);
    if (new_index > size_) {
      std::runtime_error(
          "Queue depleted!"); // TODO: Thou shall not use plain runtime_error!
    }

    const T val = *reinterpret_cast<const T *>(&(data_[index_]));
    index_ = new_index;

    return val;
  }

private:
  const uint8_t *data_; ///< Warning: Ownership resides outside of RawQueue.
  std::mutex data_mutex_;
  const size_t size_;
  size_t index_;
};

template <> std::string RawQueue::pop<std::string>() {
  return std::string("Left-out for brevity.");
};

template <typename T, typename F, typename... Args>
decltype(auto) call(RawQueue *Data, F &&f, Args &&... args) {
  if constexpr (std::is_invocable<F, Args...>::value) {
    return std::invoke(f, args...);
  } else {
    assert(Data);
    // Is there a way to deduce T automatically and for each argument
    // independently?
    auto val = Data->pop<T>();
    return call<T>(Data, f, val, args...);
  }
}

int adder(int a, int b, int c) { return a + b + c; }
std::string greeter(const std::string &name) { return "Hello, " + name + "!"; }

int mixed_arguments(int i, float f, const std::string &s) { return 42; }

int main() {
  constexpr size_t Size = 16;
  uint8_t Data[Size] = {1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0};
  RawQueue data(Data, Size);

  auto res_int = call<int>(&data, adder);
  std::cout << "Integer result: " << res_int << std::endl;

  auto res = call<std::string>(&data, greeter);
  std::cout << "String result: " << res << std::endl;

  // Impossible with current approach:
  // std::cout << "Mixed-types: " << call(&data, mixed_arguments) << std::endl;

  return 0;
}

2 ответа

Я нашел решение, используя callable.hpp. Ответы, которые не зависят от внешней библиотеки, по-прежнему приветствуются!

Соответствующее новое дополнение - это:

constexpr size_t pos = sizeof...(args);
typedef typename callable_traits<F>::template argument_type<pos> T;
auto val = Data->pop<T>();

Полный пример:

#include <cassert>
#include <functional>
#include <iostream>
#include <mutex>
#include <string>
#include <type_traits>

#include "external/callable/callable.hpp"

class RawQueue {
public:
  RawQueue(uint8_t *Data, size_t Size)
      : data_(Data), size_(Size / sizeof(uint8_t)), index_(0){};

  /** Takes one element of type T from queue.
  *
  * Throws if empty.
  *
  * NOTE: Little endianess means that uint8_t {1, 0, 0, 0} == int {1}.
  */
  template <typename T> T pop() {
    assert(data_);
    std::scoped_lock<std::mutex> lock(data_mutex_);

    const size_t new_index = index_ + sizeof(T) / sizeof(uint8_t);
    if (new_index > size_) {
      // TODO: Thou shall not use plain runtime_error!
      std::runtime_error("Queue depleted!");
    }

    const T val = *reinterpret_cast<const T *>(&(data_[index_]));
    index_ = new_index;

    return val;
  }

private:
  const uint8_t *data_; ///< Warning: Ownership resides outside of RawQueue.
  std::mutex data_mutex_;
  const size_t size_;
  size_t index_;
};

template <> std::string RawQueue::pop<std::string>() {
  std::scoped_lock<std::mutex> lock(data_mutex_);
  assert(data_);
  assert(index_ < size_);
  size_t string_length = data_[index_]; // Up-to 255 ought to be enough.
  const size_t new_index =
      index_ + string_length + 1; // +1 b/c first value is length of string.

  if (new_index > size_) {
    // TODO: Thou shall not use plain runtime_error!
    std::runtime_error("Queue depleted!");
  }

  const std::string val(reinterpret_cast<const char *>(&(data_[index_ + 1])),
                        string_length);
  index_ = new_index;
  return val;
};

template <typename F, typename... Args>
decltype(auto) call(RawQueue *Data, F &&f, Args &&... args) {
  if constexpr (std::is_invocable<F, Args...>::value) {
    return std::invoke(f, args...);
  } else {
    assert(Data);
    constexpr size_t n_already = sizeof...(args);
    constexpr size_t n_needed = callable_traits<F>::argc;
    static_assert(n_needed >= n_already, "Too many arguments!");
    constexpr size_t pos = n_already;
    typedef typename callable_traits<F>::template argument_type<pos> T;
    auto val = Data->pop<T>();
    return call(Data, f, args..., val);
  }
}

int adder(int a, int b, int c) { return a + b + c; }

std::string greeter(std::string a) { return "hello " + a; };

void mixed(int i, float f, std::string s) {
  std::cout << "Mixed: " << i << ", " << f << ", " << s << std::endl;
}

int main() {
  constexpr size_t Size = 28;
  // clang-format off
  uint8_t Data[Size] = {
      3, 'A', 'd', 'a',
      1, 0, 0, 0,
      2, 0, 0, 0,
      4, 0, 0, 0,
      42, 0, 0, 0,
      0xDA, 0x0F, 0x49, 0x40, // 3.141...
      3, 'P', 'i', '!'};
  // clang-format on
  RawQueue data(Data, Size);

  std::cout << "String: " << call(&data, greeter) << std::endl;
  std::cout << "Integers: " << call(&data, adder) << std::endl;
  call(&data, mixed);
  call(&data, []() { std::cout << "Nothing to do!" << std::endl; });

  return 0;
}

Печать:

String: hello Ada
Integers: 7
Mixed: 42, 3.14159, Pi!
Nothing to do!

Вы можете использовать различные шаблоны.

Если вы хотите вызвать во время выполнения произвольную функцию произвольной подписи с произвольными аргументами, вам следует рассмотреть возможность использования libffi (библиотеки интерфейса сторонней функции, которая знает ваш ABI и соглашения о вызовах).

Другие вопросы по тегам