необязательная проверка диапазона на основе параметра шаблона

Допустим, у меня есть класс, который просто выполняет сложение для любого типа T. Я хочу добавить необязательную проверку диапазона (на основе параметра шаблона типа bool), которая будет проверять, принадлежит ли результат сложения к заданному диапазону или иначе выкинет. Один из способов сделать это — обернуть все основы класса в базовый класс, а затем специализироваться на логическом параметре шаблона. Что-то вроде:

// The base class; holds a starting value to add to and a maximum value
template<typename T>
class DummyImpl
{
private:
  T mval, mmax;

public:
  constexpr explicit DummyImpl(T x, T max_x) noexcept
 : mval{x}, mmax{max_x}
  {};

  // base class; use a virtual destructor
  virtual ~DummyImpl() {};

  T max() const noexcept {return mmax;}
  T val() const noexcept {return mval;}
};

// The "real" class; parameter B denotes if we want (or not)
// a range check
template<typename T, bool B>
class Dummy : DummyImpl<T> {};

// Specialize: we do want range check; if sum not in range
// throw.
template<typename T>
class Dummy<T, true> : DummyImpl<T>
{
public:
  explicit Dummy(T x, T max_x) noexcept : DummyImpl<T>(x, max_x) {};

  T add(T x) const noexcept( !true )
  {
    T ret_val = x + DummyImpl<T>::val();
    if (ret_val < 0 || ret_val > DummyImpl<T>::max()) {
      throw 1;
    }
    return ret_val;
  }
};

// Specialize for no range check.
template<typename T>
class Dummy<T, false> : DummyImpl<T>
{
public:
  explicit Dummy(T x, T max_x) noexcept : DummyImpl<T>(x, max_x) {};

  T add(T x) const noexcept( !false )
  {
    return x + DummyImpl<T>::val();
  }
};

Теперь пользователь может написать такой код:

int main()
{
  Dummy<float,false> d(0, 1000); //no range check; never throw

  std::cout <<"\nAdding  156.7 gives " << d.add(156.7);
  std::cout <<"\nAdding 3156.7 gives " << d.add(3156.7);

  std::cout <<"\n";
  return 0;
}

Есть ли способ сделать это без использования наследования? Я бы предположил, что использование вложенного класса было бы более эффективным, но следующий код не компилируется.

template<typename T,  bool RC>
class Dummy
{
private:
  T mval, mmax;

  // parameter S is only used to enable partial specialization on
  // parameter I
  template<bool I, typename S> struct add_impl {};

  template<typename S> struct add_impl<true, S>
  {
    T operator()(T x) const noexcept( !true )
    {
      T ret_val = x + mval;
      if (ret_val < 0 || ret_val > mmax) {throw 1;}
      return ret_val;
    }
  };

  template<typename S> struct add_impl<false,  S>
  {
    T operator()(T x) const noexcept( !false )
    {
      return x + mval_ref;
    }
  };

public:
  constexpr explicit Dummy(T x, T max_x) noexcept
 : mval{x}, mmax{max_x}
  {};

  void bar() const { std::cout << "\nin Base."; }
  T max() const noexcept {return mmax;}
  T val() const noexcept {return mval;}
  T add(T x) const noexcept( !RC )
  {
    return add_impl<RC, T>()(x);
  }
};


int main()
{
  Dummy<float,false> d(0, 1000);

  std::cout <<"\nAdding  156.7 gives " << d.add(156.7);
  std::cout <<"\nAdding 3156.7 gives " << d.add(3156.7);

  std::cout <<"\n";
  return 0;
}

Сбой с сообщением об ошибке (в g++):

error: invalid use of non-static data member ‘Dummy<float, false>::mval’

Есть ли способ обойти это? Если да, то является ли оно более эффективным, чем первое решение? Увеличит ли вложенный класс размер любого экземпляра Dummy? Есть ли более элегантный дизайн/реализация?


person xnth    schedule 02.09.2015    source источник
comment
Как вы можете изменить код шаблона в зависимости от параметров: Ищите SFINAE или специализацию шаблона. Но действительно ли это полезно в вашем случае использования? Наличие логического параметра false, который ничего не говорит о том, что будет означать false, делает код нечитаемым.   -  person Klaus    schedule 02.09.2015


Ответы (4)


Я бы просто отправил RC. И сделать его типом:

template<typename T,  bool RC>
class Dummy
{
private:
    using do_range_check = std::integral_constant<bool, RC>;
    T mval, mmax;
};

При этом:

    T add(T x) const {
        return add(x, do_range_check{});
    }

private:    
    T add(T x, std::false_type /* range_check */) {
        return x + mval;
    }

    T add(T x, std::true_type /* range_check */) {
        T ret_val = x + mval;
        if (ret_val < 0 || ret_val > mmax) {throw 1;}
        return ret_val;
    }

Преимущество заключается в том, что это обычная функция-член — вы не разгружаетесь на какой-то другой тип, которому вам нужно передать члены. И вам не нужно специализироваться... ничего. Это здорово.

person Barry    schedule 02.09.2015
comment
Спасибо друг! Это действительно ясно и элегантно. Я думаю, что для спецификаций noexcept я могу использовать что-то вроде noexcept(do_range_check::value). Если я правильно понял, do_range_check{} дает либо std::false_type, либо std::true_type (в зависимости от RC), а не просто логическую переменную (как do_range_check::value), верно? - person xnth; 02.09.2015
comment
@xnth noexcept(!do_range_check::value), в остальном все правильно. - person Barry; 02.09.2015

Обычно я стараюсь не использовать логические флаги в функциях для переключения поведения.

Вы можете передать проверку диапазона как политику вместо параметра шаблона bool в стиле policy- на основе дизайна. Политики не нуждаются в наследовании, потому что нет никаких ограничений на тип аргументов шаблона, кроме тех, которые вытекают из их использования. Вы можете указать любой тип, который вам нравится, если он обеспечивает необходимый интерфейс. Таким образом, я могу определить два независимых класса без каких-либо отношений (наследования) и использовать их оба в качестве параметров шаблона. Недостатком является то, что Dummy<float, X> и Dummy<float, Y> - это два разных, не связанных между собой типа, и вы не можете, например. присвоить экземпляр первого типа экземпляру второго без определения оператора присваивания шаблона.

#include <stdexcept>

template<typename T>
struct NoMaxCheck
{
    NoMaxCheck(T) {}

    void check(T) const noexcept {}
};

template<typename T>
struct ThresholdChecker
{
    ThresholdChecker(T value) : mMax(value) {}

    void check(T value) const
    {
          if (value < 0 || mMax < value) {
              throw std::out_of_range("");
          }
    }
private:
    T mMax;
};

template<typename T,  typename CheckPolicy>
class Dummy
{
private:
  T mVal;
  CheckPolicy mThresholdChecker;

public:
  explicit Dummy(T x, T max_x) noexcept : mVal(x), mThresholdChecker(max_x) {};

  T add(T x) const noexcept(noexcept(mThresholdChecker.check(x)))
  {
    T ret_val = x + mVal();
    mThresholdChecker.check(ret_val);
    return ret_val;
  }
};

template<typename T,  template<typename> typename CheckPolicy>
class DummyEmptyBaseClasss: private CheckPolicy<T>
{
private:
  T mVal;

public:
  explicit DummyEmptyBaseClasss(T x, T max_x) noexcept:
      CheckPolicy<T>(max_x),
      mVal(x) {};

  T add(T x) const noexcept(noexcept(check(x)))
  {
    T ret_val = x + mVal();
    check(ret_val);
    return ret_val;
  }
};

int foo()
{
  Dummy<float,NoMaxCheck<float>> unchecked(0, 1000);
  Dummy<float,ThresholdChecker<float>> checked(0, 1000);
  static_assert( sizeof(DummyEmptyBaseClasss<float, NoMaxCheck>) == sizeof(float), "empty base class optimization");
}

Вы можете упростить его с помощью параметров шаблона-шаблона, чтобы избавиться от избыточного параметра с плавающей запятой. DummyEmptyBaseClass показывает это.

person Jens    schedule 02.09.2015
comment
Код не будет компилироваться. Я не понимаю структуру NoMaxCheck. Предполагается ли, что оно происходит от ThresholdChecker? Если CheckPolicy равен NoMaxCheck, станет ли экземпляр объекта Dummy больше, чем необходимо, или сработает оптимизация пустой базы? - person xnth; 02.09.2015
comment
@xnth Если исправлен код примера и расширено объяснение. Если вы измените Dummy на частное наследование от CheckPolicy, оптимизация пустого базового класса должна устранить накладные расходы пространства. - person Jens; 02.09.2015
comment
@xnth Я добавил пример с оптимизацией пустого базового класса. - person Jens; 02.09.2015

Компиляторы довольно хорошо удаляют явно мертвый код (например, возникающий из логических параметров шаблона). Поэтому я бы выбрал самое простое решение:

template<typename T,  bool RC>
class Dummy
{
private:
  T mval, mmax;

public:
  T add(T x) const noexcept( !RC )
  {
    T ret_val = x + val();
    if (RC && (ret_val < 0 || ret_val > DummyImpl<T>::max())) {
      throw 1;
    }
    return ret_val;
  }

//...
};

Я был бы чрезвычайно удивлен, если бы какой-либо код среды выполнения был сгенерирован для экземпляра, где RC == false. На самом деле, я бы посчитал это ошибкой оптимизатора.

person Angew is no longer proud of SO    schedule 02.09.2015
comment
@Agnew, спасибо, приятель; это был мой первоначальный дизайн, но я не был уверен, улучшит ли компилятор это. Казалось бы, банальный случай. - person xnth; 02.09.2015

Вы можете использовать состав

template<typename T, bool B> struct ThresholdChecker;

template<typename T>
struct ThresholdChecker<T, true>
{
    ThresholdChecker(T value) : mMax(value) {}

    void check(T value) const
    {
          if (value < 0 || mMax < value) {
              throw std::out_of_range("");
          }
    }
private:
    T mMax;
};

template<typename T>
struct ThresholdChecker<T, false>
{
    ThresholdChecker(T) {}

    void check(T) const noexcept {}
};


template<typename T,  bool RC>
class Dummy
{
private:
  T mval;
  ThresholdChecker<T, RC> mThresholdChecker;

public:
  explicit Dummy(T x, T max_x) noexcept : mVal(x), mThresholdChecker(max_x) {};

  T add(T x) const noexcept(noexcept(mThresholdChecker.check(x)))
  {
    T ret_val = x + val();
    mThresholdChecker.check(ret_val);
    return ret_val;
  }

//...
};
person Jarod42    schedule 02.09.2015