2015-12-16 12 views
5

Ich habe zwei gute Ansätze gefunden, integrale Arrays zu Kompilierungszeiten here und here zu initialisieren.Wie initialisiert man ein Fließkomma-Array zur Kompilierzeit?

Leider kann keine zur Initialisierung einer Float-Array direkt konvertiert werden; Ich finde, dass ich in der Template-Metaprogrammierung nicht fit genug bin, um dies durch Trial-and-Error zu lösen.

Lassen Sie mich zunächst einen Anwendungsfall erklären:

constexpr unsigned int SineLength = 360u; 
constexpr unsigned int ArrayLength = SineLength+(SineLength/4u); 
constexpr double PI = 3.1415926535; 

float array[ArrayLength]; 

void fillArray(unsigned int length) 
{ 
    for(unsigned int i = 0u; i < length; ++i) 
    array[i] = sin(double(i)*PI/180.*360./double(SineLength)); 
} 

Wie Sie sehen können, was die Verfügbarkeit von Informationen geht, dieses Array könnteconstexpr deklariert werden.

jedoch für den ersten Ansatz verknüpft, die Generatorfunktion f würde wie folgt aussehen:

constexpr float f(unsigned int i) 
{ 
    return sin(double(i)*PI/180.*360./double(SineLength)); 
} 

Und das bedeutet, dass eine Vorlage Argument vom Typ float benötigt wird. Was nicht erlaubt ist. Die erste Idee, die mir in den Sinn kommt, wäre, den Float in einer int-Variablen zu speichern - den Array-Indizes passiert nichts nach ihrer Berechnung, so dass sie von einem anderen Typ sind als sie sind (solange Byte -Länge ist gleich) ist völlig in Ordnung.

Aber siehe:

constexpr int f(unsigned int i) 
{ 
    float output = sin(double(i)*PI/180.*360./double(SineLength)); 
    return *(int*)&output; 
} 

ist keine gültige constexpr, da sie mehr als die Rückkehr-Anweisung enthält.

constexpr int f(unsigned int i) 
{ 
    return reinterpret_cast<int>(sin(double(i)*PI/180.*360./double(SineLength))); 
} 

funktioniert auch nicht; obwohl man denken könnte, dass reinterpret_cast genau das tut, was hier gebraucht wird (nämlich nichts), funktioniert es anscheinend nur auf Zeigern.

Nach dem zweiten Ansatz, die Generatorfunktion würde etwas anders aussehen:

template<size_t index> struct f 
{ 
    enum : float{ value = sin(double(index)*PI/180.*360./double(SineLength)) }; 
}; 

Mit dem, was ist im Wesentlichen das gleiche Problem: Das ENUM kann nicht von float Typ sein und der Typ kann nicht als int maskiert werden.


Nun, auch wenn ich nur das Problem auf dem Weg der „so tun, als die float ist ein int“ genähert habe, ich weiß nicht wirklich wie dieser Pfad (abgesehen von nicht in Betrieb). Ich würde viel lieber einen Weg, der tatsächlich behandelt die float als float (und würde genauso gut eine double als double behandeln), aber ich sehe keine Möglichkeit, um die Art Einschränkung auferlegt umgehen.

Leider gibt es viele Fragen zu diesem Thema, die sich immer auf ganzzahlige Typen beziehen und die Suche nach diesem speziellen Problem überlagern. In ähnlicher Weise berücksichtigen Fragen zum Maskieren eines Typs als dem anderen typischerweise nicht die Einschränkungen einer constexpr oder Template-Parameter-Umgebung.
Siehe [2][3] und [4][5] usw.

+0

Sie werden nicht viel Glück haben eine 'constexpr' Funktion basiert auf' std :: sin() ', da diese Funktion isn Schaffung‘ t eine "conexpr" -Funktion. Vielleicht sollte es sein, aber ab sofort ist es nicht. –

+0

@ DietmarKühl Es ist ein intrinsischer in einigen Compilern. Außerdem ist die Implementierung der Serienerweiterung nicht so schwierig. – Columbo

+0

Mögliches Duplikat der [Taylor-Reihenentwicklung als constexpr] (http://stackoverflow.com/questions/34216359/taylor-series-expansion-as-constexpr) – Drop

Antwort

10

Ihr eigentliches Ziel Unter der Annahme ist eine prägnante Art und Weise hat eine Reihe von Gleitkommazahlen zu initialisieren und es ist nicht unbedingt float array[N] oder double array[N] geschrieben, sondern std::array<float, N> array oder std::array<double, N> array diese getan werden kann.

Die Bedeutung der Art der Array ist, dass std::array<T, N> kopiert werden kann - im Gegensatz zu T[N]. Wenn es kopiert werden kann, können Sie den Inhalt des Arrays aus einem Funktionsaufruf erhalten, z.B .:

constexpr std::array<float, ArrayLength> array = fillArray<N>(); 

Wie das uns hilft? Nun, wenn wir eine Funktion aufrufen können, die eine Ganzzahl als Argument verwendet, können wir std::make_index_sequence<N> verwenden, um eine Kompilierungssequenz von std::size_t von 0 bis N-1 zu verwenden. Wenn wir das haben, können wir eine Reihe leicht mit einer Formel basierend auf dem Index wie folgt initialisieren:

constexpr double const_sin(double x) { return x * 3.1; } // dummy... 
template <std::size_t... I> 
constexpr std::array<float, sizeof...(I)> fillArray(std::index_sequence<I...>) { 
    return std::array<float, sizeof...(I)>{ 
      const_sin(double(I)*M_PI/180.*360./double(SineLength))... 
     }; 
} 

template <std::size_t N> 
constexpr std::array<float, N> fillArray() { 
    return fillArray(std::make_index_sequence<N>{}); 
} 

Angenommen, die Funktion verwendet, um die Array-Elemente zu initialisieren ist eigentlich ein constexpr Ausdruck kann dieser Ansatz ein constexpr erzeugen. Die Funktion const_sin(), die nur für Demonstrationszwecke da ist, tut das, aber sie berechnet offensichtlich keine vernünftige Annäherung an sin(x).

Die Kommentare zeigen, dass die Antwort bisher nicht ganz erklären kann, was vor sich geht. So lassen Sie sich brechen sie in verdauliche Teile:

  1. Das Ziel, eine constexpr Array mit geeigneter Folge von Werten gefüllt zu erzeugen ist. Die Größe des Arrays sollte jedoch leicht geändert werden können, indem nur die Array-Größe N angepasst wird. Das heißt, vom Konzept her ist es das Ziel

    constexpr float array[N] = { f(0), f(1), ..., f(N-1) }; 
    

    zu schaffen, wo f() eine geeignete Funktion ist ein constexpr erzeugen. Zum Beispiel könnte f() als

    constexpr float f(int i) { 
        return const_sin(double(i) * M_PI/180.0 * 360.0/double(Length); 
    } 
    

    jedoch definiert werden, die Eingabe in den Aufrufen von f(0), f(1) usw. müßte bei jeder Änderung von N ändern. Also, im Wesentlichen das gleiche wie die obige Deklaration sollte getan werden, aber ohne zusätzliche Eingabe.

  2. Der erste Schritt zur Lösung von float[N]std:array<float, N> zu ersetzen: Einbau-Arrays nicht kopiert werden kann, während std::array<float, N> kopiert werden kann. Das heißt, die Initialisierung könnte an eine durch N parametrierte Funktion delegiert werden. Das heißt, wir

    template <std::size_t N> 
    constexpr std::array<float, N> fillArray() { 
        // some magic explained below goes here 
    } 
    constexpr std::array<float, N> array = fillArray<N>(); 
    
  3. Innerhalb der Funktion verwenden würden wir nicht einfach Schleife über das Array, weil die nicht const Index-Operator ist kein constexpr. Stattdessen muss das Array bei der Erstellung initialisiert werden.Wenn wir hatte einen Parameter Pack std::size_t... I, die die Sequenz dargestellt 0, 1, .., N-1 wir konnte einfach nicht

    std::array<float, N>{ f(I)... }; 
    

    als Erweiterung effektiv äquivalent werden würde

    std::array<float, N>{ f(0), f(1), .., f(N-1) }; 
    

    So ist die Frage nach der Eingabe wird: wie get so ein Parameterpaket? Ich glaube nicht, dass es direkt in der Funktion erhalten werden kann, aber es kann erhalten werden durch den Aufruf einer anderen Funktion mit einem geeigneten Parameter. Der Alias ​​std::make_index_sequence<N> ist ein Alias ​​für den Typ std::index_sequence<0, 1, .., N-1>. Die Details der Implementierung sind ein wenig geheimnisvoll, aber std::make_index_sequence<N>, std::index_sequence<...>, und Freunde sind Teil von C++ 14 (sie wurden von N3493 basierend beispielsweise auf this answer from me vorgeschlagen). Das heißt, alles, was wir tun müssen, ist mit einem Parameter des Typs eine Hilfsfunktion aufrufen std::index_sequence<...> und den Parameter-Pack von dort:

    template <std::size_t...I> 
    constexpr std::array<float, sizeof...(I)> 
    fillArray(std::index_sequence<I...>) { 
        return std::array<float, sizeof...(I)>{ f(I)... }; 
    } 
    template <std::size_t N> 
    constexpr std::array<float, N> fillArray() { 
        return fillArray(std::make_index_sequence<N>{}); 
    } 
    

    Der [unnamed] Parameter für diese Funktion nur den Parameter Pack haben verwendet wird std::size_t... I abgeleitet werden.

+1

Sie können Ihren Code auf [Coliru] (http://coliru.stacked-crooked.com/) oder Wandbox versuchen. Beantwortet dies auch wirklich die Frage des Fragestellers? Sie können die statische Initialisierung des Obigen nicht garantieren, es sei denn, die Sünde ist intrinsisch und der Compiler entscheidet sich dafür. – Columbo

+0

@Columbo: Manchmal gibt es Einschränkungen bei der Arbeit am Arbeitsplatz. –

+0

@Columbo: statische Initialisierung ist _not_ Konstante Initialisierung. Dies geschieht zur Laufzeit nach einer konstanten Initialisierung. Die ursprüngliche Frage könnte eine konstante Initialisierung bedeuten. Da 'sin()' nicht garantiert ist, dass "constexpr" ist, kann eine konstante Initialisierung nicht garantiert werden, indem diese Funktion trotzdem verwendet wird. Wenn alle beteiligten Operationen "conexpr" sind, funktioniert dies auch für eine konstante Initialisierung. –

-2

Ich behalte nur diese Antwort für die Dokumentation. Wie die Kommentare sagen, wurde ich in die Irre geführt, weil gcc permissiv ist. Es schlägt fehl, wenn f(42) z.B. als Template-Parameter wie folgt aus:

std::array<int, f(42)> asdf; 

sorry, war dies keine Lösung

Trennen Sie die Berechnung Ihrer Schwimmer und die Umwandlung in einen int in zwei verschiedenen constexpr Funktionen:

constexpr int floatAsInt(float float_val) { 
    return *(int*)&float_val; 
} 

constexpr int f(unsigned int i) { 
    return floatAsInt(sin(double(i)*PI/180.*360./double(SineLength))); 
} 
+0

'reinterpret_cast's, wie von Ihrem Code erforderlich, sind nicht in konstanten Ausdrücken erlaubt . – Columbo

+0

Ich habe gerade versucht, diesen Code mit gcc zu kompilieren. Also ist entweder gcc permissiver als der Standard, oder meine Lösung ist in Ordnung. – cdonat

+0

Der ehemalige. GCC ist nicht konform. (Haben Sie versucht, die Funktion auszuführen? Im Moment ist Ihr Code schlecht geformt, ohne dass eine Diagnose erforderlich ist.) – Columbo

0

Hier ist ein Arbeitsbeispiel, das eine Tabelle von sin-Werten erzeugt und die Sie leicht an Logarithmentabellen anpassen können, indem Sie ein anderes Funktionsobjekt übergeben

#include <array> // array 
#include <cmath> // sin 
#include <cstddef> // size_t 
#include <utility> // index_sequence, make_index_sequence 
#include <iostream> 

namespace detail { 

template<class Function, std::size_t... Indices> 
constexpr auto make_array_helper(Function f, std::index_sequence<Indices...>) 
     -> std::array<decltype(f(std::size_t{})), sizeof...(Indices)> 
{ 
     return {{ f(Indices)... }}; 
} 

}  // namespace detail 

template<std::size_t N, class Function> 
constexpr auto make_array(Function f) 
{ 
     return detail::make_array_helper(f, std::make_index_sequence<N>{}); 
} 

static auto const pi = std::acos(-1); 
static auto const make_sin = [](int x) { return std::sin(pi * x/180.0); }; 
static auto const sin_table = make_array<360>(make_sin); 

int main() 
{ 
    for (auto elem : sin_table) 
     std::cout << elem << "\n"; 
} 

Live Example.

Beachten Sie, dass Sie benötigen -stdlib=libc++ zu verwenden, da libstdc++ eine ziemlich ineffiziente Umsetzung index_sequence hat.

Beachten Sie auch, dass Sie ein constexpr Funktionsobjekt benötigen (beide pi und std::sin sind nicht constexpr) das Array wirklich zur Compile-Zeit statt bei Programminitialisierung zu initialisieren.

+0

Wie zeigt sich die Effizienz der Implementierung für einen Ausdruck, der zur Kompilierzeit eliminiert werden soll und daher nie in einem laufenden Programm ausgewertet wird? Der einzige Teil, der es in das kompilierte Programm bringt, wäre das Array selbst, wäre es nicht? – Zsar

+0

@Zsar ja, das wäre die Idee. – TemplateRex

+0

Nun, in Ordnung, aber dann ... warum der Ratschlag, Bibliotheken zu wechseln, wenn alles nur Kompilierzeit wäre? – Zsar

1

Es gibt ein paar Probleme zu überwinden, wenn Sie einen Floating-Point-Array zum Zeitpunkt der Kompilierung initialisieren wollen:

  1. std::array ist ein wenig gebrochen, dass die operator[] nicht im Fall eines wandelbaren constexpr constexpr std :: array (Ich glaube, das wird in der nächsten Version des Standards behoben werden).

  2. Die Funktionen in std :: math sind nicht als constexpr markiert!

Ich hatte vor kurzem eine ähnliche Problemdomäne. Ich wollte eine genaue, aber schnelle Version von sin(x) erstellen.

Ich beschloss zu sehen, ob es mit einem constexpr Cache mit Interpolation gemacht werden könnte, um Geschwindigkeit ohne Genauigkeitsverlust zu erhalten.

Ein Vorteil des Cache constexpr ist, dass die Berechnung von sin(x) für einen zur Kompilierzeit bekannten Wert ist, dass sin vorberechnet ist und einfach im Code als eine unmittelbare Registerlast existiert! Im schlimmsten Fall eines Laufzeitarguments ist es lediglich eine konstante Array-Suche, gefolgt von einem gewichteten Mittelwert.

Dieser Code muss mit -fconstexpr-steps=2000000 auf Clang kompiliert werden, oder das Äquivalent in Windows.

genießen:

#include <iostream> 
#include <cmath> 
#include <utility> 
#include <cassert> 
#include <string> 
#include <vector> 

namespace cpputil { 

    // a fully constexpr version of array that allows incomplete 
    // construction 
    template<size_t N, class T> 
    struct array 
    { 
     // public constructor defers to internal one for 
     // conditional handling of missing arguments 
     constexpr array(std::initializer_list<T> list) 
     : array(list, std::make_index_sequence<N>()) 
     { 

     } 

     constexpr T& operator[](size_t i) noexcept { 
      assert(i < N); 
      return _data[i]; 
     } 

     constexpr const T& operator[](size_t i) const noexcept { 
      assert(i < N); 
      return _data[i]; 
     } 

     constexpr T& at(size_t i) noexcept { 
      assert(i < N); 
      return _data[i]; 
     } 

     constexpr const T& at(size_t i) const noexcept { 
      assert(i < N); 
      return _data[i]; 
     } 

     constexpr T* begin() { 
      return std::addressof(_data[0]); 
     } 

     constexpr const T* begin() const { 
      return std::addressof(_data[0]); 
     } 

     constexpr T* end() { 
      // todo: maybe use std::addressof and disable compiler warnings 
      // about array bounds that result 
      return &_data[N]; 
     } 

     constexpr const T* end() const { 
      return &_data[N]; 
     } 

     constexpr size_t size() const { 
      return N; 
     } 

    private: 

     T _data[N]; 

    private: 

     // construct each element from the initialiser list if present 
     // if not, default-construct 
     template<size_t...Is> 
     constexpr array(std::initializer_list<T> list, std::integer_sequence<size_t, Is...>) 
     : _data { 
      (
      Is >= list.size() 
      ? 
      T() 
      : 
      std::move(*(std::next(list.begin(), Is))) 
      )... 
     } 
     { 

     } 
    }; 

    // convenience printer 
    template<size_t N, class T> 
    inline std::ostream& operator<<(std::ostream& os, const array<N, T>& a) 
    { 
     os << "["; 
     auto sep = " "; 
     for (const auto& i : a) { 
      os << sep << i; 
      sep = ", "; 
     } 
     return os << " ]"; 
    } 

} 


namespace trig 
{ 
    constexpr double pi() { 
     return M_PI; 
    } 

    template<class T> 
    auto constexpr to_radians(T degs) { 
     return degs/180.0 * pi(); 
    } 

    // compile-time computation of a factorial 
    constexpr double factorial(size_t x) { 
     double result = 1.0; 
     for (int i = 2 ; i <= x ; ++i) 
      result *= double(i); 
     return result; 
    } 

    // compile-time replacement for std::pow 
    constexpr double power(double x, size_t n) 
    { 
     double result = 1; 
     while (n--) { 
      result *= x; 
     } 
     return result; 
    } 

    // compute a term in a taylor expansion at compile time 
    constexpr double taylor_term(double x, size_t i) 
    { 
     int powers = 1 + (2 * i); 
     double top = power(x, powers); 
     double bottom = factorial(powers); 
     auto term = top/bottom; 
     if (i % 2 == 1) 
      term = -term; 
     return term; 
    } 

    // compute the sin(x) using `terms` terms in the taylor expansion   
    constexpr double taylor_expansion(double x, size_t terms) 
    { 
     auto result = x; 
     for (int term = 1 ; term < terms ; ++term) 
     { 
      result += taylor_term(x, term); 
     } 
     return result; 
    } 

    // compute our interpolatable table as a constexpr 
    template<size_t N = 1024> 
    struct sin_table : cpputil::array<N, double> 
    { 
     static constexpr size_t cache_size = N; 
     static constexpr double step_size = (pi()/2)/cache_size; 
     static constexpr double _360 = pi() * 2; 
     static constexpr double _270 = pi() * 1.5; 
     static constexpr double _180 = pi(); 
     static constexpr double _90 = pi()/2; 

     constexpr sin_table() 
     : cpputil::array<N, double>({}) 
     { 
      for(int slot = 0 ; slot < cache_size ; ++slot) 
      { 
       double val = trig::taylor_expansion(step_size * slot, 20); 
       (*this)[slot] = val; 
      } 
     } 

     double checked_interp_fw(double rads) const { 
      size_t slot0 = size_t(rads/step_size); 
      auto v0 = (slot0 >= this->size()) ? 1.0 : (*this)[slot0]; 

      size_t slot1 = slot0 + 1; 
      auto v1 = (slot1 >= this->size()) ? 1.0 : (*this)[slot1]; 

      auto ratio = (rads - (slot0 * step_size))/step_size; 

      return (v1 * ratio) + (v0 * (1.0-ratio)); 
     } 

     double interpolate(double rads) const 
     { 
      rads = std::fmod(rads, _360); 
      if (rads < 0) 
       rads = std::fmod(_360 - rads, _360); 

      if (rads < _90) { 
       return checked_interp_fw(rads); 
      } 
      else if (rads < _180) { 
       return checked_interp_fw(_90 - (rads - _90)); 
      } 
      else if (rads < _270) { 
       return -checked_interp_fw(rads - _180); 
      } 
      else { 
       return -checked_interp_fw(_90 - (rads - _270)); 
      } 
     } 


    }; 

    double sine(double x) 
    { 
     if (x < 0) { 
      return -sine(-x); 
     } 
     else { 
      constexpr sin_table<> table; 
      return table.interpolate(x); 
     } 
    } 

} 


void check(float degs) { 
    using namespace std; 

    cout << "checking : " << degs << endl; 
    auto mysin = trig::sine(trig::to_radians(degs)); 
    auto stdsin = std::sin(trig::to_radians(degs)); 
    auto error = stdsin - mysin; 
    cout << "mine=" << mysin << ", std=" << stdsin << ", error=" << error << endl; 
    cout << endl; 
} 

auto main() -> int 
{ 

    check(0.5); 
    check(30); 
    check(45.4); 
    check(90); 
    check(151); 
    check(180); 
    check(195); 
    check(89.5); 
    check(91); 
    check(270); 
    check(305); 
    check(360); 
    return 0; 
} 

erwartete Ausgabe:

checking : 0.5 
mine=0.00872653, std=0.00872654, error=2.15177e-09 

checking : 30 
mine=0.5, std=0.5, error=1.30766e-07 

checking : 45.4 
mine=0.712026, std=0.712026, error=2.07233e-07 

checking : 90 
mine=1, std=1, error=0 

checking : 151 
mine=0.48481, std=0.48481, error=2.42041e-08 

checking : 180 
mine=-0, std=1.22465e-16, error=1.22465e-16 

checking : 195 
mine=-0.258819, std=-0.258819, error=-6.76265e-08 

checking : 89.5 
mine=0.999962, std=0.999962, error=2.5215e-07 

checking : 91 
mine=0.999847, std=0.999848, error=2.76519e-07 

checking : 270 
mine=-1, std=-1, error=0 

checking : 305 
mine=-0.819152, std=-0.819152, error=-1.66545e-07 

checking : 360 
mine=0, std=-2.44929e-16, error=-2.44929e-16 
Verwandte Themen