หากฉันมีคำประกาศดังต่อไปนี้:
float a = 3.0 ;
เป็นข้อผิดพลาดหรือไม่? ผมอ่านในหนังสือที่3.0
เป็นคุณค่าและว่าผมจะต้องระบุว่าเป็นdouble
float a = 3.0f
มันเป็นอย่างนั้นเหรอ?
หากฉันมีคำประกาศดังต่อไปนี้:
float a = 3.0 ;
เป็นข้อผิดพลาดหรือไม่? ผมอ่านในหนังสือที่3.0
เป็นคุณค่าและว่าผมจะต้องระบุว่าเป็นdouble
float a = 3.0f
มันเป็นอย่างนั้นเหรอ?
;
หลัง
คำตอบ:
ไม่ใช่ข้อผิดพลาดในการประกาศfloat a = 3.0
: ถ้าคุณทำคอมไพเลอร์จะแปลง 3.0 ลิเทอรัลคู่เป็นโฟลตให้คุณ
อย่างไรก็ตามคุณควรใช้สัญกรณ์ตัวอักษรลอยในสถานการณ์เฉพาะ
ด้วยเหตุผลด้านประสิทธิภาพ:
โดยเฉพาะให้พิจารณา:
float foo(float x) { return x * 0.42; }
ที่นี่คอมไพเลอร์จะปล่อยการแปลง (ซึ่งคุณจะจ่ายเมื่อรันไทม์) สำหรับแต่ละค่าที่ส่งคืน เพื่อหลีกเลี่ยงสิ่งนี้คุณควรประกาศ:
float foo(float x) { return x * 0.42f; } // OK, no conversion required
เพื่อหลีกเลี่ยงข้อบกพร่องเมื่อเปรียบเทียบผลลัพธ์:
เช่นการเปรียบเทียบต่อไปนี้ล้มเหลว:
float x = 4.2;
if (x == 4.2)
std::cout << "oops"; // Not executed!
เราสามารถแก้ไขได้ด้วยสัญกรณ์ตัวอักษรลอย:
if (x == 4.2f)
std::cout << "ok !"; // Executed!
(หมายเหตุ: แน่นอนว่านี่ไม่ใช่วิธีที่คุณควรเปรียบเทียบจำนวนทศนิยมหรือเลขคู่เพื่อความเท่าเทียมกันโดยทั่วไป )
ในการเรียกใช้ฟังก์ชันโอเวอร์โหลดที่ถูกต้อง (ด้วยเหตุผลเดียวกัน):
ตัวอย่าง:
void foo(float f) { std::cout << "\nfloat"; }
void foo(double d) { std::cout << "\ndouble"; }
int main()
{
foo(42.0); // calls double overload
foo(42.0f); // calls float overload
return 0;
}
ตามที่ระบุไว้ใน Cyberในบริบทการหักแบบประเภทจำเป็นที่จะต้องช่วยคอมไพเลอร์อนุมาน a float
:
ในกรณีauto
:
auto d = 3; // int
auto e = 3.0; // double
auto f = 3.0f; // float
และในทำนองเดียวกันในกรณีของการหักประเภทเทมเพลต:
void foo(float f) { std::cout << "\nfloat"; }
void foo(double d) { std::cout << "\ndouble"; }
template<typename T>
void bar(T t)
{
foo(t);
}
int main()
{
bar(42.0); // Deduce double
bar(42.0f); // Deduce float
return 0;
}
42
เป็นจำนวนเต็มซึ่งได้รับการเลื่อนตำแหน่งโดยอัตโนมัติfloat
(และจะเกิดขึ้นในเวลาคอมไพล์ในคอมไพเลอร์ที่เหมาะสม) ดังนั้นจึงไม่มีการลงโทษด้านประสิทธิภาพ 42.0
คุณอาจหมายถึงสิ่งที่ต้องการ
4.2
เป็น4.2f
อาจมีผลข้างเคียงของการตั้งค่าFE_INEXACT
แฟล็กขึ้นอยู่กับคอมไพเลอร์และระบบและบางโปรแกรม (ยอมรับน้อยมาก) จะสนใจว่าการดำเนินการทศนิยมใดที่แน่นอนและไม่เป็นเช่นนั้นและทดสอบแฟล็กนั้น . ซึ่งหมายความว่าการแปลงเวลาคอมไพล์อย่างง่ายที่ชัดเจนจะเปลี่ยนแปลงพฤติกรรมของโปรแกรม
float foo(float x) { return x*42.0; }
สามารถรวบรวมเป็นการคูณที่มีความแม่นยำเดียวและรวบรวมโดย Clang ในครั้งสุดท้ายที่ฉันลอง อย่างไรก็ตามfloat foo(float x) { return x*0.1; }
ไม่สามารถรวบรวมเป็นการคูณที่มีความแม่นยำเดียวได้ ก่อนหน้านี้แพตช์นี้อาจจะมองโลกในแง่ดีไปหน่อย แต่หลังจากแพตช์แล้วควรรวม Conversion-double_precision_op-conversion เป็น single_precision_op เมื่อผลลัพธ์เป็นแบบเดียวกันเสมอ article.gmane.org/gmane.comp.compilers.llvm.cvs/167800/match=
someFloat
นิพจน์someFloat * 0.1
จะได้ผลลัพธ์ที่แม่นยำกว่าsomeFloat * 0.1f
ในขณะที่ในหลาย ๆ กรณีมีราคาถูกกว่าการหารทศนิยม ตัวอย่างเช่น (float) (167772208.0f * 0.1) จะปัดเศษเป็น 16777220 ได้อย่างถูกต้องแทนที่จะเป็น 16777222 คอมไพเลอร์บางตัวอาจแทนที่การdouble
คูณสำหรับการหารทศนิยม แต่สำหรับผู้ที่ไม่มี (ปลอดภัยสำหรับหลายค่าแม้ว่าจะไม่ใช่ทุกค่าก็ตาม ) การคูณอาจเป็นการเพิ่มประสิทธิภาพที่มีประโยชน์ แต่ถ้าดำเนินการโดยใช้double
การแลกเปลี่ยน
คอมไพเลอร์จะเปลี่ยนลิเทอรัลใด ๆ ต่อไปนี้ให้เป็นโฟลตเนื่องจากคุณประกาศตัวแปรเป็นโฟลต
float a = 3; // converted to float
float b = 3.0; // converted to float
float c = 3.0f; // float
มันจะสำคัญถ้าคุณใช้auto
(หรือวิธีการหักแบบอื่น ๆ ) ตัวอย่างเช่น:
auto d = 3; // int
auto e = 3.0; // double
auto f = 3.0f; // float
auto
ไม่ใช่กรณีเดียว
ตัวอักษรทศนิยมที่ไม่มีคำต่อท้ายเป็นประเภทdoubleซึ่งครอบคลุมอยู่ในส่วนมาตรฐาน C ++ แบบร่าง2.14.4
ตัวอักษรลอย :
[... ] ประเภทของลิเทอรัลลอยเป็นสองเท่าเว้นแต่จะระบุไว้อย่างชัดเจนโดยคำต่อท้าย [... ]
ดังนั้นมันจึงเป็นข้อผิดพลาดในการกำหนดอักษรคู่ไปลอย :3.0
float a = 3.0
ไม่มันจะถูกแปลงซึ่งครอบคลุมอยู่ในหัวข้อ4.8
การแปลงทศนิยม :
ค่า prvalue ของประเภทจุดลอยตัวสามารถแปลงเป็นราคาล่วงหน้าของประเภทจุดลอยตัวอื่นได้ หากสามารถแสดงค่าต้นทางในประเภทปลายทางได้ทั้งหมดผลลัพธ์ของการแปลงคือการแสดงที่แน่นอน หากค่าต้นทางอยู่ระหว่างค่าปลายทางสองค่าที่อยู่ติดกันผลลัพธ์ของการแปลงจะเป็นตัวเลือกที่กำหนดโดยการนำไปใช้ของค่าเหล่านั้น มิฉะนั้นจะไม่มีการกำหนดพฤติกรรม
เราสามารถอ่านรายละเอียดเพิ่มเติมเกี่ยวกับผลกระทบของสิ่งนี้ได้ในGotW # 67: double or nothingซึ่งระบุว่า:
ซึ่งหมายความว่าค่าคงที่สองเท่าสามารถแปลงเป็นค่าคงที่แบบลอยตัวได้โดยปริยายแม้ว่าการทำเช่นนั้นจะสูญเสียความแม่นยำ (เช่นข้อมูล) สิ่งนี้ได้รับอนุญาตให้คงอยู่เนื่องจากเหตุผลด้านความเข้ากันได้ของ C และการใช้งาน แต่ก็ควรคำนึงถึงเมื่อคุณทำงานแบบลอยตัว
คอมไพเลอร์ที่มีคุณภาพจะเตือนคุณหากคุณพยายามทำอะไรบางอย่างที่ไม่ได้กำหนดลักษณะการทำงานกล่าวคือใส่ปริมาณสองเท่าลงในจำนวนที่น้อยกว่าค่าต่ำสุดหรือมากกว่าค่าสูงสุดที่โฟลทสามารถแทนได้ คอมไพเลอร์ที่ดีจริงๆจะให้คำเตือนทางเลือกหากคุณพยายามทำบางสิ่งบางอย่างที่อาจกำหนดไว้ แต่อาจสูญเสียข้อมูลกล่าวคือใส่ปริมาณสองเท่าลงในทศนิยมที่อยู่ระหว่างค่าต่ำสุดและค่าสูงสุดที่แทนค่าได้โดยลอย แต่ไม่สามารถ จะแสดงเป็นลอย
ดังนั้นจึงมีข้อแม้สำหรับกรณีทั่วไปที่คุณควรทราบ
จากมุมมองในทางปฏิบัติในกรณีนี้ผลลัพธ์มักจะเหมือนกันแม้ว่าในทางเทคนิคจะมีการแปลงเราสามารถเห็นสิ่งนี้ได้โดยลองใช้รหัสต่อไปนี้บนgodbolt :
#include <iostream>
float func1()
{
return 3.0; // a double literal
}
float func2()
{
return 3.0f ; // a float literal
}
int main()
{
std::cout << func1() << ":" << func2() << std::endl ;
return 0;
}
และเราจะเห็นว่าผลลัพธ์สำหรับfunc1
และfunc2
เหมือนกันโดยใช้ทั้งสองclang
และgcc
:
func1():
movss xmm0, DWORD PTR .LC0[rip]
ret
func2():
movss xmm0, DWORD PTR .LC0[rip]
ret
ดังที่Pascal ชี้ให้เห็นในความคิดเห็นนี้คุณจะไม่สามารถนับสิ่งนี้ได้เสมอไป การใช้0.1
และ0.1f
ตามลำดับทำให้แอสเซมบลีที่สร้างขึ้นแตกต่างกันเนื่องจากตอนนี้ต้องทำการแปลงอย่างชัดเจน รหัสต่อไปนี้:
float func1(float x )
{
return x*0.1; // a double literal
}
float func2(float x)
{
return x*0.1f ; // a float literal
}
ผลลัพธ์ในการประกอบต่อไปนี้:
func1(float):
cvtss2sd %xmm0, %xmm0 # x, D.31147
mulsd .LC0(%rip), %xmm0 #, D.31147
cvtsd2ss %xmm0, %xmm0 # D.31147, D.31148
ret
func2(float):
mulss .LC2(%rip), %xmm0 #, D.31155
ret
ไม่ว่าคุณจะสามารถระบุได้ว่าการแปลงจะมีผลกระทบต่อประสิทธิภาพหรือไม่โดยใช้ประเภทที่ถูกต้องจะดีกว่าเอกสารความตั้งใจของคุณ ตัวอย่างเช่นการใช้การแปลงอย่างชัดแจ้งstatic_cast
ยังช่วยชี้แจงว่าการแปลงมีจุดประสงค์เพื่อไม่ให้เกิดอุบัติเหตุซึ่งอาจบ่งบอกถึงข้อบกพร่องหรือข้อบกพร่อง
บันทึก
ดังที่ supercat ชี้ให้เห็นการคูณด้วยเช่น0.1
และ0.1f
ไม่เทียบเท่า ฉันแค่จะอ้างความคิดเห็นเพราะมันยอดเยี่ยมและบทสรุปอาจไม่ยุติธรรม:
ตัวอย่างเช่นถ้า f เท่ากับ 100000224 (ซึ่งแทนค่าได้ว่าเป็นค่าลอย) การคูณด้วยหนึ่งในสิบควรให้ผลลัพธ์ที่ปัดเศษลงเป็น 10000022 แต่การคูณด้วย 0.1f จะให้ผลลัพธ์ที่ปัดเศษได้ถึง 10000023 อย่างผิดพลาด ถ้าตั้งใจจะหารด้วยสิบการคูณด้วยค่าคงที่คู่ 0.1 น่าจะเร็วกว่าการหารด้วย 10f และแม่นยำกว่าการคูณด้วย 0.1f
จุดเริ่มต้นของฉันคือการแสดงให้เห็นถึงตัวอย่างที่ผิดพลาดที่ให้ไว้ในคำถามอื่น แต่สิ่งนี้แสดงให้เห็นถึงปัญหาที่ละเอียดอ่อนในตัวอย่างของเล่น
f = f * 0.1;
และทำสิ่งที่แตกต่างกันf = f * 0.1f;
ตัวอย่างเช่นถ้าf
เท่ากับ 100000224 (ซึ่งแทนค่าได้อย่างแน่นอนว่า a float
) การคูณด้วยหนึ่งในสิบควรให้ผลลัพธ์ที่ปัดเศษลงเป็น 10000022 แต่การคูณด้วย 0.1f จะให้ผลลัพธ์ที่ปัดเศษได้สูงถึง 10000023 อย่างผิดพลาดหาก ความตั้งใจที่จะหารด้วยสิบคูณโดยdouble
คงที่ 0.1 มีแนวโน้มที่จะเร็วกว่าการหารด้วยและแม่นยำมากขึ้นกว่าการคูณโดย10f
0.1f
ไม่ใช่ข้อผิดพลาดในแง่ที่คอมไพเลอร์จะปฏิเสธ แต่เป็นข้อผิดพลาดในแง่ที่อาจไม่ใช่สิ่งที่คุณต้องการ
ในฐานะที่เป็นหนังสือของคุณได้อย่างถูกต้องกล่าวคือค่าของชนิด3.0
double
มีการแปลงโดยนัยจากdouble
เป็นfloat
ดังนั้นfloat a = 3.0;
คำจำกัดความที่ถูกต้องของตัวแปร
อย่างไรก็ตามอย่างน้อยในแนวความคิดสิ่งนี้จะทำการเปลี่ยนใจเลื่อมใสโดยไม่จำเป็น การแปลงอาจดำเนินการในเวลาคอมไพล์หรืออาจบันทึกไว้สำหรับรันไทม์ทั้งนี้ขึ้นอยู่กับคอมไพลเลอร์ เหตุผลที่ถูกต้องในการบันทึกเป็นเวลาทำงานคือการแปลงทศนิยมเป็นเรื่องยากและอาจมีผลข้างเคียงที่ไม่คาดคิดหากไม่สามารถแสดงค่าได้อย่างถูกต้องและไม่ใช่เรื่องง่ายเสมอไปที่จะตรวจสอบว่าสามารถแสดงค่าได้อย่างถูกต้องหรือไม่
3.0f
หลีกเลี่ยงปัญหานั้น: แม้ว่าในทางเทคนิคแล้วคอมไพเลอร์ยังคงได้รับอนุญาตให้คำนวณค่าคงที่ในขณะรัน (เป็นเสมอ) ที่นี่ไม่มีเหตุผลอย่างแน่นอนว่าทำไมคอมไพเลอร์ใด ๆ จึงอาจทำเช่นนั้นได้
แม้ว่าจะไม่ใช่ข้อผิดพลาด แต่ก็ค่อนข้างเลอะเทอะเล็กน้อย คุณรู้ว่าคุณต้องการลอยดังนั้นเริ่มต้นด้วยการลอย
โปรแกรมเมอร์คนอื่นอาจเข้ามาและไม่แน่ใจว่าส่วนใดของการประกาศที่ถูกต้องประเภทหรือตัวเริ่มต้น ทำไมไม่มีทั้งสองอย่างถูกต้อง?
คำตอบลอย = 42.0f;
เมื่อคุณกำหนดตัวแปรตัวแปรจะเริ่มต้นด้วย initializer ที่ให้มา ซึ่งอาจต้องมีการแปลงค่าของ initializer เป็นประเภทของตัวแปรที่กำลังเริ่มต้น นั่นคือสิ่งที่เกิดขึ้นเมื่อคุณพูดว่าfloat a = 3.0;
: ค่าของการเริ่มต้นที่จะถูกแปลงและผลของการแปลงจะกลายเป็นค่าเริ่มต้นของfloat
a
นั่นคือโดยทั่วไปดี แต่มันไม่ได้เจ็บที่จะเขียนเพื่อแสดงว่าคุณกำลังตระหนักถึงสิ่งที่คุณกำลังทำและโดยเฉพาะอย่างยิ่งถ้าคุณต้องการที่จะเขียน3.0f
auto a = 3.0f
หากคุณลองทำสิ่งต่อไปนี้:
std::cout << sizeof(3.2f) <<":" << sizeof(3.2) << std::endl;
คุณจะได้ผลลัพธ์เป็น:
4:8
ที่แสดงให้เห็นว่าขนาด 3.2f ถูกใช้เป็น 4 ไบต์บนเครื่อง 32 บิตเมื่อ 3.2 ถูกตีความว่าเป็นค่าสองเท่าโดยใช้ 8 ไบต์บนเครื่อง 32 บิต สิ่งนี้ควรให้คำตอบที่คุณกำลังมองหา
double
และfloat
แตกต่างกันก็ไม่ได้ตอบว่าคุณสามารถเริ่มต้น a float
จากตัวอักษรคู่ได้หรือไม่
คอมไพเลอร์จะอนุมานประเภทที่เหมาะสมที่สุดออกจากตัวอักษรหรือโดยไม่คำนึงถึงสิ่งที่คิดว่าเหมาะสมที่สุด นั่นค่อนข้างจะสูญเสียประสิทธิภาพมากกว่าความแม่นยำกล่าวคือใช้ double แทนการลอย หากมีข้อสงสัยให้ใช้ brace-intializers เพื่อทำให้ชัดเจน:
auto d = double{3}; // make a double
auto f = float{3}; // make a float
auto i = int{3}; // make a int
เรื่องราวจะน่าสนใจยิ่งขึ้นหากคุณเริ่มต้นจากตัวแปรอื่นที่ใช้กฎการแปลงประเภท: แม้ว่าจะถูกกฎหมายที่จะสร้างรูปแบบสองเท่าเป็นตัวอักษร แต่ก็ไม่สามารถสร้างโครงสร้างจาก int ได้โดยไม่ต้อง จำกัด ให้แคบลง:
auto xxx = double{i} // warning ! narrowing conversion of 'i' from 'int' to 'double'
3.0
เป็นโฟลตให้คุณfloat a = 3.0f
ผลลัพธ์ที่ได้คือแยกไม่ออกจาก